aboutsummaryrefslogtreecommitdiff
path: root/src/js/thirdparty/ws.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/thirdparty/ws.js')
-rw-r--r--src/js/thirdparty/ws.js1119
1 files changed, 1119 insertions, 0 deletions
diff --git a/src/js/thirdparty/ws.js b/src/js/thirdparty/ws.js
new file mode 100644
index 000000000..3af650764
--- /dev/null
+++ b/src/js/thirdparty/ws.js
@@ -0,0 +1,1119 @@
+// Hardcoded module "ws"
+// Mocking https://github.com/websockets/ws
+// this just wraps WebSocket to look like an EventEmitter
+// without actually using an EventEmitter polyfill
+
+import EventEmitter from "events";
+import http from "http";
+
+const kBunInternals = Symbol.for("::bunternal::");
+const readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
+const encoder = new TextEncoder();
+
+const emittedWarnings = new Set();
+function emitWarning(type, message) {
+ if (emittedWarnings.has(type)) return;
+ emittedWarnings.add(type);
+ // process.emitWarning(message); // our printing is bad
+ console.warn("[bun] Warning:", message);
+}
+
+/*
+ * deviations: we do not implement these events
+ * - "unexpected-response"
+ * - "upgrade"
+ * - "ping"
+ * - "pong"
+ * - "redirect"
+ */
+class BunWebSocket extends EventEmitter {
+ static CONNECTING = 0;
+ static OPEN = 1;
+ static CLOSING = 2;
+ static CLOSED = 3;
+
+ #ws;
+ #paused = false;
+ #fragments = false;
+ #binaryType = "nodebuffer";
+ readyState = BunWebSocket.CONNECTING;
+
+ constructor(url, protocols, options) {
+ // deviation: we don't support anything in `options`
+ super();
+ let ws = (this.#ws = new WebSocket(url, protocols));
+ ws.binaryType = "nodebuffer"; // bun's WebSocket supports "nodebuffer"
+ ws.addEventListener("open", () => {
+ this.readyState = BunWebSocket.OPEN;
+ this.emit("open");
+ });
+ ws.addEventListener("error", err => {
+ this.readyState = BunWebSocket.CLOSED;
+ this.emit("error", err);
+ });
+ ws.addEventListener("close", ev => {
+ this.readyState = BunWebSocket.CLOSED;
+ this.emit("close", ev.code, ev.reason);
+ });
+ ws.addEventListener("message", ev => {
+ const isBinary = typeof ev.data !== "string";
+ if (isBinary) {
+ this.emit("message", this.#fragments ? [ev.data] : ev.data, isBinary);
+ } else {
+ var encoded = encoder.encode(ev.data);
+ if (this.#binaryType !== "arraybuffer") {
+ encoded = Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength);
+ }
+ this.emit("message", this.#fragments ? [encoded] : encoded, isBinary);
+ }
+ });
+ }
+
+ on(event, listener) {
+ if (
+ event === "unexpected-response" ||
+ event === "upgrade" ||
+ event === "ping" ||
+ event === "pong" ||
+ event === "redirect"
+ ) {
+ emitWarning(event, "ws.WebSocket '" + event + "' event is not implemented in bun");
+ }
+ return super.on(event, listener);
+ }
+
+ send(data, opts, cb) {
+ this.#ws.send(data, opts?.compress);
+ // deviation: this should be called once the data is written, not immediately
+ typeof cb === "function" && cb();
+ }
+
+ close(code, reason) {
+ this.#ws.close(code, reason);
+ }
+
+ get binaryType() {
+ return this.#binaryType;
+ }
+
+ set binaryType(value) {
+ if (value) this.#ws.binaryType = value;
+ }
+
+ set binaryType(value) {
+ if (value === "nodebuffer" || value === "arraybuffer") {
+ this.#ws.binaryType = this.#binaryType = value;
+ this.#fragments = false;
+ } else if (value === "fragments") {
+ this.#ws.binaryType = "nodebuffer";
+ this.#binaryType = "fragments";
+ this.#fragments = true;
+ }
+ }
+
+ get protocol() {
+ return this.#ws.protocol;
+ }
+
+ get extensions() {
+ return this.#ws.extensions;
+ }
+
+ // deviation: this does not support `message` with `binaryType = "fragments"`
+ addEventListener(type, listener, options) {
+ this.#ws.addEventListener(type, listener, options);
+ }
+
+ removeEventListener(type, listener) {
+ this.#ws.removeEventListener(type, listener);
+ }
+
+ get onopen() {
+ return this.#ws.onopen;
+ }
+
+ set onopen(value) {
+ this.#ws.onopen = value;
+ }
+
+ get onerror() {
+ return this.#ws.onerror;
+ }
+
+ set onerror(value) {
+ this.#ws.onerror = value;
+ }
+
+ get onclose() {
+ return this.#ws.onclose;
+ }
+
+ set onclose(value) {
+ this.#ws.onclose = value;
+ }
+
+ get onmessage() {
+ return this.#ws.onmessage;
+ }
+
+ // deviation: this does not support `binaryType = "fragments"`
+ set onmessage(value) {
+ this.#ws.onmessage = value;
+ }
+
+ get bufferedAmount() {
+ return this.#ws.bufferedAmount;
+ }
+
+ get isPaused() {
+ return this.#paused;
+ }
+
+ ping(data, mask, cb) {
+ if (this.readyState === BunWebSocket.CONNECTING) {
+ throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");
+ }
+
+ if (typeof data === "function") {
+ cb = data;
+ data = mask = undefined;
+ } else if (typeof mask === "function") {
+ cb = mask;
+ mask = undefined;
+ }
+
+ if (typeof data === "number") data = data.toString();
+
+ // deviation: we don't support ping
+ emitWarning("ping()", "ws.WebSocket.ping() is not implemented in bun");
+ typeof cb === "function" && cb();
+ }
+
+ pong(data, mask, cb) {
+ if (this.readyState === BunWebSocket.CONNECTING) {
+ throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");
+ }
+
+ if (typeof data === "function") {
+ cb = data;
+ data = mask = undefined;
+ } else if (typeof mask === "function") {
+ cb = mask;
+ mask = undefined;
+ }
+
+ if (typeof data === "number") data = data.toString();
+
+ // deviation: we don't support pong
+ emitWarning("pong()", "ws.WebSocket.pong() is not implemented in bun");
+ typeof cb === "function" && cb();
+ }
+
+ pause() {
+ if (this.readyState === WebSocket.CONNECTING || this.readyState === WebSocket.CLOSED) {
+ return;
+ }
+
+ this.#paused = true;
+
+ // deviation: we dont support pause()
+ emitWarning("pause()", "ws.WebSocket.pause() is not implemented in bun");
+ }
+
+ resume() {
+ if (this.readyState === WebSocket.CONNECTING || this.readyState === WebSocket.CLOSED) {
+ return;
+ }
+
+ this.#paused = false;
+
+ // deviation: we dont support resume()
+ emitWarning("resume()", "ws.WebSocket.resume() is not implemented in bun");
+ }
+}
+
+BunWebSocket.WebSocket = BunWebSocket;
+
+const wsKeyRegex = /^[+/0-9A-Za-z]{22}==$/;
+const wsTokenChars = [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 0 - 15
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 16 - 31
+ 0,
+ 1,
+ 0,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0,
+ 0,
+ 1,
+ 1,
+ 0,
+ 1,
+ 1,
+ 0, // 32 - 47
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0, // 48 - 63
+ 0,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 64 - 79
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ 1, // 80 - 95
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1, // 96 - 111
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0,
+ 1,
+ 0,
+ 1,
+ 0, // 112 - 127
+];
+
+/**
+ * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names.
+ *
+ * @param {String} header The field value of the header
+ * @return {Set} The subprotocol names
+ * @public
+ */
+function subprotocolParse(header) {
+ const protocols = new Set();
+ let start = -1;
+ let end = -1;
+ let i = 0;
+
+ for (i; i < header.length; i++) {
+ const code = header.charCodeAt(i);
+
+ if (end === -1 && wsTokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (i !== 0 && (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */) {
+ if (end === -1 && start !== -1) end = i;
+ } else if (code === 0x2c /* ',' */) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+
+ const protocol = header.slice(start, end);
+
+ if (protocols.has(protocol)) {
+ throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
+ }
+
+ protocols.add(protocol);
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ }
+
+ if (start === -1 || end !== -1) {
+ throw new SyntaxError("Unexpected end of input");
+ }
+
+ const protocol = header.slice(start, i);
+
+ if (protocols.has(protocol)) {
+ throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
+ }
+
+ protocols.add(protocol);
+ return protocols;
+}
+
+/**
+ * Emit a `'close'` event on an `EventEmitter`.
+ *
+ * @param {EventEmitter} server The event emitter
+ * @private
+ */
+function wsEmitClose(server) {
+ server._state = CLOSED;
+ server.emit("close");
+}
+
+function abortHandshake(response, code, message, headers) {
+ message = message || http.STATUS_CODES[code];
+ headers = {
+ Connection: "close",
+ "Content-Type": "text/html",
+ "Content-Length": Buffer.byteLength(message),
+ ...headers,
+ };
+
+ response.writeHead(code, headers);
+ response.write(message);
+ response.end();
+}
+
+function abortHandshakeOrEmitwsClientError(server, req, response, socket, code, message) {
+ if (server.listenerCount("wsClientError")) {
+ const err = new Error(message);
+ Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
+
+ server.emit("wsClientError", err, socket, req);
+ } else {
+ abortHandshake(response, code, message);
+ }
+}
+
+const RUNNING = 0;
+const CLOSING = 1;
+const CLOSED = 2;
+
+class BunWebSocketMocked extends EventEmitter {
+ #ws;
+ #state;
+ #enquedMessages = [];
+ #url;
+ #protocol;
+ #extensions;
+ #bufferedAmount = 0;
+ #binaryType = "arraybuffer";
+
+ #onclose;
+ #onerror;
+ #onmessage;
+ #onopen;
+
+ constructor(url, protocol, extensions, binaryType) {
+ super();
+ this.#ws = null;
+ this.#state = 0;
+ this.#url = url;
+ this.#bufferedAmount = 0;
+ binaryType = binaryType || "arraybuffer";
+ if (binaryType !== "nodebuffer" && binaryType !== "blob" && binaryType !== "arraybuffer") {
+ throw new TypeError("binaryType must be either 'blob', 'arraybuffer' or 'nodebuffer'");
+ }
+ this.#binaryType = binaryType;
+ this.#protocol = protocol;
+ this.#extensions = extensions;
+
+ const message = this.#message.bind(this);
+ const open = this.#open.bind(this);
+ const close = this.#close.bind(this);
+ const drain = this.#drain.bind(this);
+
+ this[kBunInternals] = {
+ message, // a message is received
+ open, // a socket is opened
+ close, // a socket is closed
+ drain, // the socket is ready to receive more data
+ };
+ }
+
+ #message(ws, message) {
+ this.#ws = ws;
+
+ if (typeof message === "string") {
+ if (this.#binaryType === "arraybuffer") {
+ message = encoder.encode(message).buffer;
+ } else if (this.#binaryType === "blob") {
+ message = new Blob([message], { type: "text/plain" });
+ } else {
+ // nodebuffer
+ message = Buffer.from(message);
+ }
+ } else {
+ //Buffer
+ if (this.#binaryType !== "nodebuffer") {
+ if (this.#binaryType === "arraybuffer") {
+ message = new Uint8Array(message);
+ } else if (this.#binaryType === "blob") {
+ message = new Blob([message]);
+ }
+ }
+ }
+
+ this.emit("message", message);
+ }
+
+ #open(ws) {
+ this.#ws = ws;
+ this.#state = 1;
+ this.emit("open", this);
+ // first drain event
+ this.#drain(ws);
+ }
+
+ #close(ws, code, reason) {
+ this.#state = 3;
+ this.#ws = null;
+
+ this.emit("close", code, reason);
+ }
+
+ #drain(ws) {
+ const chunk = this.#enquedMessages[0];
+ if (chunk) {
+ const [data, compress, cb] = chunk;
+ const written = ws.send(data, compress);
+ if (written == -1) {
+ // backpressure wait until next drain event
+ return;
+ }
+
+ typeof cb === "function" && cb();
+
+ this.#bufferedAmount -= chunk.length;
+ this.#enquedMessages.shift();
+ }
+ }
+
+ send(data, opts, cb) {
+ if (this.#state === 1) {
+ const compress = opts?.compress;
+ const written = this.#ws.send(data, compress);
+ if (written == -1) {
+ // backpressure
+ this.#enquedMessages.push([data, compress, cb]);
+ this.#bufferedAmount += data.length;
+ return;
+ }
+
+ typeof cb === "function" && cb();
+ } else if (this.#state === 0) {
+ // not connected yet
+ this.#enquedMessages.push([data, opts?.compress, cb]);
+ this.#bufferedAmount += data.length;
+ }
+ }
+
+ close(code, reason) {
+ if (this.#state === 1) {
+ this.#state = 2;
+ this.#ws.close(code, reason);
+ }
+ }
+ get binaryType() {
+ return this.#binaryType;
+ }
+
+ set binaryType(type) {
+ if (type !== "nodebuffer" && type !== "blob" && type !== "arraybuffer") {
+ throw new TypeError("binaryType must be either 'blob', 'arraybuffer' or 'nodebuffer'");
+ }
+ this.#binaryType = type;
+ }
+
+ get readyState() {
+ return readyStates[this.#state];
+ }
+ get url() {
+ return this.#url;
+ }
+
+ get protocol() {
+ return this.#protocol;
+ }
+
+ get extensions() {
+ return this.#extensions;
+ }
+
+ get bufferedAmount() {
+ return this.#bufferedAmount ?? 0;
+ }
+ /**
+ * Set up the socket and the internal resources.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Object} options Options object
+ * @param {Function} [options.generateMask] The function used to generate the
+ * masking key
+ * @param {Number} [options.maxPayload=0] The maximum allowed message size
+ * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
+ * not to skip UTF-8 validation for text and close messages
+ * @private
+ */
+ setSocket(socket, head, options) {
+ throw new Error("Not implemented");
+ }
+
+ set onclose(cb) {
+ if (this.#onclose) {
+ this.removeListener("close", this.#onclose);
+ }
+ this.on("close", cb);
+ this.#onclose = cb;
+ }
+
+ set onerror(cb) {
+ if (this.#onerror) {
+ this.removeListener("error", this.#onerror);
+ }
+ this.on("error", cb);
+ this.#onerror = cb;
+ }
+
+ set onmessage(cb) {
+ if (this.#onmessage) {
+ this.removeListener("message", this.#onmessage);
+ }
+ this.on("message", cb);
+ this.#onmessage = cb;
+ }
+
+ set onopen(cb) {
+ if (this.#onopen) {
+ this.removeListener("open", this.#onopen);
+ }
+ this.on("open", cb);
+ this.#onopen = cb;
+ }
+
+ get onclose() {
+ return this.#onclose;
+ }
+
+ get onerror() {
+ return this.#onerror;
+ }
+
+ get onmessage() {
+ return this.#onmessage;
+ }
+
+ get onopen() {
+ return this.#onopen;
+ }
+}
+
+class Server extends EventEmitter {
+ _server;
+ options;
+ clients;
+ _shouldEmitClose;
+ _state;
+ _removeListeners;
+
+ /**
+ * Create a `WebSocketServer` instance.
+ *
+ * @param {Object} options Configuration options
+ * @param {Number} [options.backlog=511] The maximum length of the queue of
+ * pending connections
+ * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
+ * track clients
+ * @param {Function} [options.handleProtocols] A hook to handle protocols
+ * @param {String} [options.host] The hostname where to bind the server
+ * size
+ * @param {Boolean} [options.noServer=false] Enable no server mode
+ * @param {String} [options.path] Accept only connections matching this path
+ * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
+ * permessage-deflate
+ * @param {Number} [options.port] The port where to bind the server
+ * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
+ * @param {Function} [options.verifyClient] A hook to reject connections
+ * class to use. It must be the `WebSocket` class or class that extends it
+ * @param {Function} [callback] A listener for the `listening` event
+ */
+ constructor(options, callback) {
+ super();
+
+ options = {
+ maxPayload: 100 * 1024 * 1024,
+ skipUTF8Validation: false,
+ perMessageDeflate: false,
+ handleProtocols: null,
+ clientTracking: true,
+ verifyClient: null,
+ noServer: false,
+ backlog: null, // use default (511 as implemented in net.js)
+ server: null,
+ host: null,
+ path: null,
+ port: null,
+ ...options,
+ };
+
+ if (
+ (options.port == null && !options.server && !options.noServer) ||
+ (options.port != null && (options.server || options.noServer)) ||
+ (options.server && options.noServer)
+ ) {
+ throw new TypeError('One and only one of the "port", "server", or "noServer" options must be specified');
+ }
+
+ if (options.port != null) {
+ this._server = http.createServer((req, res) => {
+ const body = http.STATUS_CODES[426];
+
+ res.writeHead(426, {
+ "Content-Length": body.length,
+ "Content-Type": "text/plain",
+ });
+ res.end(body);
+ });
+
+ this._server.listen(options.port, options.host, options.backlog, callback);
+ } else if (options.server) {
+ this._server = options.server;
+ }
+
+ if (this._server) {
+ const emitConnection = this.emit.bind(this, "connection");
+ const emitListening = this.emit.bind(this, "listening");
+ const emitError = this.emit.bind(this, "error");
+ const doUpgrade = (req, socket, head) => {
+ this.handleUpgrade(req, socket, head, emitConnection);
+ };
+
+ this._server.on("listening", emitListening);
+ this._server.on("error", emitError);
+ this._server.on("upgrade", doUpgrade);
+
+ this._removeListeners = () => {
+ this._server.removeListener("upgrade", doUpgrade);
+ this._server.removeListener("listening", emitListening);
+ this._server.removeListener("error", emitError);
+ };
+ }
+
+ if (options.perMessageDeflate === true) options.perMessageDeflate = {};
+ if (options.clientTracking) {
+ this.clients = new Set();
+ this._shouldEmitClose = false;
+ }
+
+ this.options = options;
+ this._state = RUNNING;
+ }
+
+ /**
+ * Returns the bound address, the address family name, and port of the server
+ * as reported by the operating system if listening on an IP socket.
+ * If the server is listening on a pipe or UNIX domain socket, the name is
+ * returned as a string.
+ *
+ * @return {(Object|String|null)} The address of the server
+ * @public
+ */
+ address() {
+ if (this.options.noServer) {
+ throw new Error('The server is operating in "noServer" mode');
+ }
+
+ if (!this._server) return null;
+ return this._server.address();
+ }
+
+ /**
+ * Stop the server from accepting new connections and emit the `'close'` event
+ * when all existing connections are closed.
+ *
+ * @param {Function} [cb] A one-time listener for the `'close'` event
+ * @public
+ */
+ close(cb) {
+ if (this._state === CLOSED) {
+ if (cb) {
+ this.once("close", () => {
+ cb(new Error("The server is not running"));
+ });
+ }
+
+ process.nextTick(server => {
+ server._state = CLOSED;
+ server.emit("close");
+ }, this);
+ return;
+ }
+
+ if (cb) this.once("close", cb);
+
+ if (this._state === CLOSING) return;
+ this._state = CLOSING;
+
+ if (this.options.noServer || this.options.server) {
+ if (this._server) {
+ this._removeListeners();
+ this._removeListeners = this._server = null;
+ }
+
+ if (this.clients) {
+ if (!this.clients.size) {
+ process.nextTick(server => {
+ server._state = CLOSED;
+ server.emit("close");
+ }, this);
+ } else {
+ this._shouldEmitClose = true;
+ }
+ } else {
+ process.nextTick(server => {
+ server._state = CLOSED;
+ server.emit("close");
+ }, this);
+ }
+ } else {
+ const server = this._server;
+
+ this._removeListeners();
+ this._removeListeners = this._server = null;
+
+ //
+ // The HTTP/S server was created internally. Close it, and rely on its
+ // `'close'` event.
+ //
+ server.close(() => {
+ this._state = CLOSED;
+ this.emit("close");
+ });
+ }
+ }
+
+ /**
+ * See if a given request should be handled by this server instance.
+ *
+ * @param {http.IncomingMessage} req Request object to inspect
+ * @return {Boolean} `true` if the request is valid, else `false`
+ * @public
+ */
+ shouldHandle(req) {
+ if (this.options.path) {
+ const index = req.url.indexOf("?");
+ const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
+
+ if (pathname !== this.options.path) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Upgrade the connection to WebSocket.
+ *
+ * @param {Object} extensions The accepted extensions
+ * @param {String} key The value of the `Sec-WebSocket-Key` header
+ * @param {Set} protocols The subprotocols
+ * @param {http.IncomingMessage} request The request object
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @throws {Error} If called more than once with the same socket
+ * @private
+ */
+ completeUpgrade(extensions, key, protocols, request, socket, head, cb) {
+ const [server, response, req] = socket[kBunInternals];
+ if (this._state > RUNNING) return abortHandshake(response, 503);
+
+ let protocol = "";
+ if (protocols.size) {
+ //
+ // Optionally call external protocol selection handler.
+ //
+ protocol = this.options.handleProtocols
+ ? this.options.handleProtocols(protocols, request)
+ : protocols.values().next().value;
+ }
+ const ws = new BunWebSocketMocked(request.url, protocol, extensions, "nodebuffer");
+
+ const headers = ["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade"];
+ this.emit("headers", headers, request);
+
+ if (
+ server.upgrade(req, {
+ data: ws[kBunInternals],
+ })
+ ) {
+ response._reply(undefined);
+ if (this.clients) {
+ this.clients.add(ws);
+ ws.on("close", () => {
+ this.clients.delete(ws);
+
+ if (this._shouldEmitClose && !this.clients.size) {
+ process.nextTick(wsEmitClose, this);
+ }
+ });
+ }
+ cb(ws, request);
+ } else {
+ abortHandshake(response, 500);
+ }
+ }
+ /**
+ * Handle a HTTP Upgrade request.
+ *
+ * @param {http.IncomingMessage} req The request object
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @public
+ */
+ handleUpgrade(req, socket, head, cb) {
+ // socket is actually fake so we use internal http_res
+ const [_, response] = socket[kBunInternals];
+
+ // socket.on("error", socketOnError);
+
+ const key = req.headers["sec-websocket-key"];
+ const version = +req.headers["sec-websocket-version"];
+
+ if (req.method !== "GET") {
+ const message = "Invalid HTTP method";
+ abortHandshakeOrEmitwsClientError(this, req, response, socket, 405, message);
+ return;
+ }
+
+ if (req.headers.upgrade.toLowerCase() !== "websocket") {
+ const message = "Invalid Upgrade header";
+ abortHandshakeOrEmitwsClientError(this, req, response, socket, 400, message);
+ return;
+ }
+
+ if (!key || !wsKeyRegex.test(key)) {
+ const message = "Missing or invalid Sec-WebSocket-Key header";
+ abortHandshakeOrEmitwsClientError(this, req, response, socket, 400, message);
+ return;
+ }
+
+ if (version !== 8 && version !== 13) {
+ const message = "Missing or invalid Sec-WebSocket-Version header";
+ abortHandshakeOrEmitwsClientError(this, req, response, socket, 400, message);
+ return;
+ }
+
+ if (!this.shouldHandle(req)) {
+ abortHandshake(response, 400);
+ return;
+ }
+
+ const secWebSocketProtocol = req.headers["sec-websocket-protocol"];
+ let protocols = new Set();
+
+ if (secWebSocketProtocol !== undefined) {
+ try {
+ protocols = subprotocolParse(secWebSocketProtocol);
+ } catch (err) {
+ const message = "Invalid Sec-WebSocket-Protocol header";
+ abortHandshakeOrEmitwsClientError(this, req, response, socket, 400, message);
+ return;
+ }
+ }
+
+ // TODO: add perMessageDeflate options
+
+ // const secWebSocketExtensions = req.headers["sec-websocket-extensions"];
+ const extensions = {};
+
+ // if (secWebSocketExtensions !== undefined) {
+ // console.log(secWebSocketExtensions);
+ // const perMessageDeflate = new PerMessageDeflate(this.options.perMessageDeflate, true, this.options.maxPayload);
+
+ // try {
+ // const offers = extension.parse(secWebSocketExtensions);
+
+ // if (offers[PerMessageDeflate.extensionName]) {
+ // perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
+ // extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
+ // }
+ // } catch (err) {
+ // const message = "Invalid or unacceptable Sec-WebSocket-Extensions header";
+ // abortHandshakeOrEmitwsClientError(this, req, response, socket, 400, message);
+ // return;
+ // }
+ // }
+
+ //
+ // Optionally call external client verification handler.
+ //
+ if (this.options.verifyClient) {
+ const info = {
+ origin: req.headers[`${version === 8 ? "sec-websocket-origin" : "origin"}`],
+ secure: !!(req.socket.authorized || req.socket.encrypted),
+ req,
+ };
+
+ if (this.options.verifyClient.length === 2) {
+ this.options.verifyClient(info, (verified, code, message, headers) => {
+ if (!verified) {
+ return abortHandshake(response, code || 401, message, headers);
+ }
+
+ this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
+ });
+ return;
+ }
+
+ if (!this.options.verifyClient(info)) return abortHandshake(response, 401);
+ }
+
+ this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
+ }
+}
+
+BunWebSocket.WebSocketServer = Server;
+BunWebSocket.Server = Server;
+
+Object.defineProperty(BunWebSocket, "CONNECTING", {
+ enumerable: true,
+ value: readyStates.indexOf("CONNECTING"),
+});
+
+Object.defineProperty(BunWebSocket.prototype, "CONNECTING", {
+ enumerable: true,
+ value: readyStates.indexOf("CONNECTING"),
+});
+
+Object.defineProperty(BunWebSocket, "OPEN", {
+ enumerable: true,
+ value: readyStates.indexOf("OPEN"),
+});
+
+Object.defineProperty(BunWebSocket.prototype, "OPEN", {
+ enumerable: true,
+ value: readyStates.indexOf("OPEN"),
+});
+
+Object.defineProperty(BunWebSocket, "CLOSING", {
+ enumerable: true,
+ value: readyStates.indexOf("CLOSING"),
+});
+
+Object.defineProperty(BunWebSocket.prototype, "CLOSING", {
+ enumerable: true,
+ value: readyStates.indexOf("CLOSING"),
+});
+
+Object.defineProperty(BunWebSocket, "CLOSED", {
+ enumerable: true,
+ value: readyStates.indexOf("CLOSED"),
+});
+
+Object.defineProperty(BunWebSocket.prototype, "CLOSED", {
+ enumerable: true,
+ value: readyStates.indexOf("CLOSED"),
+});
+
+class Sender {
+ constructor() {
+ throw new Error("Not supported yet in Bun");
+ }
+}
+
+BunWebSocket.Sender = Sender;
+
+class Receiver {
+ constructor() {
+ throw new Error("Not supported yet in Bun");
+ }
+}
+
+BunWebSocket.Receiver = Receiver;
+
+var createWebSocketStream = ws => {
+ throw new Error("Not supported yet in Bun");
+};
+
+BunWebSocket.createWebSocketStream = createWebSocketStream;
+
+BunWebSocket[Symbol.for("CommonJS")] = 0;
+
+export default BunWebSocket;
+export { createWebSocketStream, Server, Receiver, Sender, BunWebSocket as WebSocket, Server as WebSocketServer };