aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile12
-rw-r--r--bench/socketio/client.js61
-rw-r--r--bench/socketio/server.js13
-rw-r--r--src/bun.js/http.exports.js48
-rw-r--r--src/bun.js/ws.exports.js821
-rw-r--r--test/js/third_party/socket.io/fixtures/server-close.ts11
-rw-r--r--test/js/third_party/socket.io/package.json10
-rw-r--r--test/js/third_party/socket.io/socket.io-close.test.ts216
-rw-r--r--test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts319
-rw-r--r--test/js/third_party/socket.io/socket.io-handshake.test.ts118
-rw-r--r--test/js/third_party/socket.io/socket.io-messaging-many.test.ts766
-rw-r--r--test/js/third_party/socket.io/socket.io-middleware.test.ts299
-rw-r--r--test/js/third_party/socket.io/socket.io-namespaces.test.ts925
-rw-r--r--test/js/third_party/socket.io/socket.io-server-attachment.test.ts235
-rw-r--r--test/js/third_party/socket.io/socket.io-socket-middleware.test.ts89
-rw-r--r--test/js/third_party/socket.io/socket.io-socket-timeout.test.ts137
-rw-r--r--test/js/third_party/socket.io/socket.io-utility-methods.test.ts181
-rw-r--r--test/js/third_party/socket.io/socket.io-uws.test.ts208
-rw-r--r--test/js/third_party/socket.io/socket.io.test.ts1554
-rw-r--r--test/js/third_party/socket.io/support/bun.pngbin0 -> 19767 bytes
-rw-r--r--test/js/third_party/socket.io/support/util.ts86
-rw-r--r--test/package.json5
22 files changed, 6100 insertions, 14 deletions
diff --git a/Makefile b/Makefile
index 3806b2be5..a2f7d6caf 100644
--- a/Makefile
+++ b/Makefile
@@ -77,6 +77,7 @@ ZIG ?= $(shell which zig 2>/dev/null || echo -e "error: Missing zig. Please make
# so if that's resolved, it won't build for C++
REAL_CC = $(shell which clang-15 2>/dev/null || which clang 2>/dev/null)
REAL_CXX = $(shell which clang++-15 2>/dev/null || which clang++ 2>/dev/null)
+CLANG_FORMAT = $(shell which clang-format-15 2>/dev/null || which clang-format 2>/dev/null)
CC = $(REAL_CC)
CXX = $(REAL_CXX)
@@ -557,11 +558,11 @@ builtins: ## to generate builtins
$(PYTHON) $(realpath $(WEBKIT_DIR)/Source/JavaScriptCore/Scripts/generate-js-builtins.py) -i $(realpath src)/bun.js/builtins/js -o $(realpath src)/bun.js/builtins/cpp --framework WebCore --force
$(PYTHON) $(realpath $(WEBKIT_DIR)/Source/JavaScriptCore/Scripts/generate-js-builtins.py) -i $(realpath src)/bun.js/builtins/js -o $(realpath src)/bun.js/builtins/cpp --framework WebCore --wrappers-only
rm -rf /tmp/1.h src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h.1
- echo -e '// clang-format off\nnamespace Zig { class GlobalObject; }\n#include "root.h"\n' >> /tmp/1.h
+ echo -e '// $(CLANG_FORMAT) off\nnamespace Zig { class GlobalObject; }\n#include "root.h"\n' >> /tmp/1.h
cat /tmp/1.h src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h > src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h.1
mv src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h.1 src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h
rm -rf /tmp/1.h src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h.1
- echo -e '// clang-format off\nnamespace Zig { class GlobalObject; }\n#include "root.h"\n' >> /tmp/1.h
+ echo -e '// $(CLANG_FORMAT) off\nnamespace Zig { class GlobalObject; }\n#include "root.h"\n' >> /tmp/1.h
cat /tmp/1.h src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.cpp > src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.cpp.1
mv src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.cpp.1 src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.cpp
$(SED) -i -e 's/class JSDOMGlobalObject/using JSDOMGlobalObject = Zig::GlobalObject/' src/bun.js/builtins/cpp/WebCoreJSBuiltinInternals.h
@@ -797,7 +798,7 @@ release-safe: prerelease release-safe-only
.PHONY: fmt-cpp
fmt-cpp:
- cd src/bun.js/bindings && clang-format *.cpp *.h -i
+ cd src/bun.js/bindings && $(CLANG_FORMAT) *.cpp *.h -i
.PHONY: fmt-zig
fmt-zig:
@@ -923,7 +924,6 @@ jsc-bindings: headers bindings
clone-submodules:
git -c submodule."src/bun.js/WebKit".update=none submodule update --init --recursive --depth=1 --progress
-CLANG_FORMAT := $(shell command -v clang-format 2> /dev/null)
.PHONY: headers
headers:
@@ -1447,11 +1447,11 @@ wasm-return1:
generate-classes:
bun src/bun.js/scripts/generate-classes.ts
$(ZIG) fmt src/bun.js/bindings/generated_classes.zig
- clang-format -i src/bun.js/bindings/ZigGeneratedClasses.h src/bun.js/bindings/ZigGeneratedClasses.cpp
+ $(CLANG_FORMAT) -i src/bun.js/bindings/ZigGeneratedClasses.h src/bun.js/bindings/ZigGeneratedClasses.cpp
generate-sink:
bun src/bun.js/scripts/generate-jssink.js
- clang-format -i src/bun.js/bindings/JSSink.cpp src/bun.js/bindings/JSSink.h
+ $(CLANG_FORMAT) -i src/bun.js/bindings/JSSink.cpp src/bun.js/bindings/JSSink.h
$(WEBKIT_DIR)/Source/JavaScriptCore/create_hash_table src/bun.js/bindings/JSSink.cpp > src/bun.js/bindings/JSSinkLookupTable.h
$(SED) -i -e 's/#include "Lookup.h"//' src/bun.js/bindings/JSSinkLookupTable.h
$(SED) -i -e 's/namespace JSC {//' src/bun.js/bindings/JSSinkLookupTable.h
diff --git a/bench/socketio/client.js b/bench/socketio/client.js
new file mode 100644
index 000000000..066a06483
--- /dev/null
+++ b/bench/socketio/client.js
@@ -0,0 +1,61 @@
+const { io } = require("socket.io-client");
+const port = process.env.PORT || 3000;
+
+const URL = `ws://localhost:${port}`;
+const MAX_CLIENTS = 250;
+const BATCHSIZE = MAX_CLIENTS / 10;
+const BATCH_INTERVAL_IN_MS = 1000;
+const EMIT_INTERVAL_IN_MS = 50;
+
+let clientCount = 0;
+let lastReport = new Date().getTime();
+let packetsSinceLastReport = 0;
+
+const clients = [];
+const createClient = () => {
+ const socket = io(URL);
+ clients.push(socket);
+
+ socket.on("server to client event", () => {
+ packetsSinceLastReport++;
+ });
+
+ socket.on("disconnect", reason => {
+ console.log(`disconnect due to ${reason}`);
+ });
+};
+
+let emitInterval = null;
+
+const createClients = () => {
+ for (let i = 0; i < BATCHSIZE; i++) {
+ createClient();
+ clientCount++;
+ }
+
+ if (clientCount < MAX_CLIENTS) {
+ setTimeout(createClients, BATCH_INTERVAL_IN_MS);
+ }
+ if (!emitInterval) {
+ emitInterval = setInterval(() => {
+ clients.forEach(socket => {
+ socket.emit("client to server event", "hello world");
+ });
+ }, EMIT_INTERVAL_IN_MS);
+ }
+};
+
+createClients();
+
+const printReport = () => {
+ const now = new Date().getTime();
+ const durationSinceLastReport = (now - lastReport) / 1000;
+ const packetsPerSeconds = (packetsSinceLastReport / durationSinceLastReport).toFixed(2);
+
+ console.log(`client count: ${clientCount} ; average packets received per second: ${packetsPerSeconds}`);
+
+ packetsSinceLastReport = 0;
+ lastReport = now;
+};
+
+setInterval(printReport, 1000);
diff --git a/bench/socketio/server.js b/bench/socketio/server.js
new file mode 100644
index 000000000..21252f192
--- /dev/null
+++ b/bench/socketio/server.js
@@ -0,0 +1,13 @@
+const http = require("http").createServer();
+
+const io = require("socket.io")(http);
+const port = process.env.PORT || 3000;
+io.on("connection", socket => {
+ socket.on("client to server event", msg => {
+ io.emit("server to client event", msg);
+ });
+});
+
+http.listen(port, () => {
+ console.log(`Socket.IO server running at http://localhost:${port}/`);
+});
diff --git a/src/bun.js/http.exports.js b/src/bun.js/http.exports.js
index 8176c1260..42a583dd6 100644
--- a/src/bun.js/http.exports.js
+++ b/src/bun.js/http.exports.js
@@ -1,4 +1,5 @@
const { EventEmitter } = import.meta.require("node:events");
+const { isIPv6 } = import.meta.require("node:net");
const { Readable, Writable, Duplex } = import.meta.require("node:stream");
const { URL } = import.meta.require("node:url");
const { newArrayWithSize, String, Object, Array } = import.meta.primordials;
@@ -39,7 +40,9 @@ const NODE_HTTP_WARNING =
var _globalAgent;
var _defaultHTTPSAgent;
var kInternalRequest = Symbol("kInternalRequest");
+var kInternalSocketData = Symbol.for("::bunternal::");
+const kEmptyBuffer = Buffer.alloc(0);
var FakeSocket = class Socket extends Duplex {
bytesRead = 0;
bytesWritten = 0;
@@ -318,7 +321,14 @@ export class Server extends EventEmitter {
}
address() {
- return this.#server?.hostname;
+ if (!this.#server) return null;
+
+ const address = this.#server.hostname;
+ return {
+ address,
+ family: isIPv6(address) ? "IPv6" : "IPv4",
+ port: this.#server.port,
+ };
}
listen(port, host, onListen) {
@@ -347,8 +357,30 @@ export class Server extends EventEmitter {
this.#server = Bun.serve({
port,
hostname: host,
-
- fetch(req) {
+ // Bindings to be used for WS Server
+ websocket: {
+ open(ws) {
+ if (ws.data && typeof ws.data.open === "function") {
+ ws.data.open(ws);
+ }
+ },
+ message(ws, message) {
+ if (ws.data && typeof ws.data.message === "function") {
+ ws.data.message(ws, message);
+ }
+ },
+ close(ws, code, reason) {
+ if (ws.data && typeof ws.data.close === "function") {
+ ws.data.close(ws, code, reason);
+ }
+ },
+ drain(ws) {
+ if (ws.data && typeof ws.data.drain === "function") {
+ ws.data.drain(ws);
+ }
+ },
+ },
+ fetch(req, _server) {
var pendingResponse;
var pendingError;
var rejectFunction, resolveFunction;
@@ -369,7 +401,15 @@ export class Server extends EventEmitter {
http_req.once("error", err => reject(err));
http_res.once("error", err => reject(err));
- server.emit("request", http_req, http_res);
+
+ const upgrade = req.headers.get("upgrade");
+ if (upgrade) {
+ const socket = new FakeSocket();
+ socket[kInternalSocketData] = [_server, http_res];
+ server.emit("upgrade", http_req, socket, kEmptyBuffer);
+ } else {
+ server.emit("request", http_req, http_res);
+ }
if (pendingError) {
throw pendingError;
diff --git a/src/bun.js/ws.exports.js b/src/bun.js/ws.exports.js
index 864ac20d3..9c6fc0aea 100644
--- a/src/bun.js/ws.exports.js
+++ b/src/bun.js/ws.exports.js
@@ -2,6 +2,11 @@
// 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::");
+
class BunWebSocket extends globalThis.WebSocket {
constructor(url, ...args) {
super(url, ...args);
@@ -64,9 +69,819 @@ class BunWebSocket extends globalThis.WebSocket {
BunWebSocket.WebSocket = BunWebSocket;
-class Server {
- constructor() {
- throw new Error("Not supported yet in Bun");
+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;
+const OPEN_EVENT = new Event("open");
+
+class BunWebSocketMocked extends EventEmitter {
+ #ws;
+ #state;
+ #enquedMessages = [];
+ #url;
+ #protocol;
+ #extensions;
+ #bufferedAmount = 0;
+ #binaryType = "blob";
+
+ #onclose;
+ #onerror;
+ #onmessage;
+ #onopen;
+
+ constructor(url, protocol, extensions) {
+ super();
+ this.#ws = null;
+ this.#state = 0;
+ this.#url = url;
+ this.#bufferedAmount = 0;
+ this.#binaryType = "blob";
+ this.#protocol = protocol;
+ this.#extensions = extensions;
+ this.binaryType = binaryType;
+
+ const message = this.#message.bind(this);
+ const open = this.#open.bind(this);
+ const close = this.#close.bind(this);
+ const drain = this.#drain.bind(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 === "blob") {
+ message = new Blob([message], { type: "text/plain" });
+ } else if (this.#binaryType === "arraybuffer") {
+ const encoder = new TextEncoder();
+ message = encoder.encode(message).buffer;
+ } else {
+ // nodebuffer or buffer
+ message = Buffer.from(message);
+ }
+ } else {
+ //Uint8Array
+ if (this.#binaryType === "blob") {
+ message = new Blob([message]);
+ } else if (this.#binaryType === "arraybuffer") {
+ message = message.buffer;
+ } else {
+ // nodebuffer or buffer
+ message = Buffer.from(message);
+ }
+ }
+ this.emit(
+ "message",
+ new MessageEvent("message", {
+ cancelable: false,
+ data: message,
+ source: this,
+ }),
+ );
+ }
+
+ #open(ws) {
+ this.#ws = ws;
+ this.#state = 1;
+ this.emit("open", OPEN_EVENT);
+ }
+
+ #close(ws, code, reason) {
+ this.#ws = null;
+ this.#state = 3;
+
+ this.emit(
+ "close",
+ new CloseEvent("close", {
+ code,
+ reason,
+ wasClean: true,
+ }),
+ );
+ }
+
+ #drain(ws) {
+ this.#ws = ws;
+ const data = this.#enquedMessages[0];
+ const length = data.length - start;
+ const written = ws.send(data.slice(start));
+ if (written == -1) {
+ // backpressure wait until next drain event
+ return;
+ }
+
+ this.#bufferedAmount -= length;
+ this.#enquedMessages.shift();
+ }
+
+ send(data) {
+ if (this.#state !== 1) {
+ return;
+ }
+ const written = this.#ws.send(data);
+ if (written == -1) {
+ const length = data.length;
+ // backpressure
+ this.#enquedMessages.push([data, length]);
+ this.#bufferedAmount += length;
+ return;
+ }
+ }
+
+ 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 !== "buffer" && type !== "blob" && type !== "arraybuffer") {
+ throw new TypeError("binaryType must be either 'blob', 'arraybuffer', 'nodebuffer' or 'buffer'");
+ }
+ this.#binaryType = type;
+ }
+
+ get readyState() {
+ return 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] = 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);
+
+ const headers = ["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade"];
+
+ this.emit("headers", headers, req);
+
+ if (
+ server.upgrade(request.req, {
+ data: ws,
+ headers,
+ })
+ ) {
+ 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) {
+ // 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);
}
}
diff --git a/test/js/third_party/socket.io/fixtures/server-close.ts b/test/js/third_party/socket.io/fixtures/server-close.ts
new file mode 100644
index 000000000..e5d47bba8
--- /dev/null
+++ b/test/js/third_party/socket.io/fixtures/server-close.ts
@@ -0,0 +1,11 @@
+const server = require("http").createServer();
+const ioc = require("socket.io-client");
+const io = require("socket.io")(server);
+
+const srv = server.listen(() => {
+ const socket = ioc.connect("ws://localhost:" + server.address().port);
+ socket.on("connect", () => {
+ io.close();
+ socket.close();
+ });
+});
diff --git a/test/js/third_party/socket.io/package.json b/test/js/third_party/socket.io/package.json
new file mode 100644
index 000000000..edeb84845
--- /dev/null
+++ b/test/js/third_party/socket.io/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "socket.io",
+ "version": "1.0.0",
+ "dependencies": {
+ "socket.io": "^4.6.1",
+ "socket.io-client": "^4.6.1",
+ "supertest": "^6.1.6",
+ "uWebSockets.js": "uNetworking/uWebSockets.js#v20.24.0"
+ }
+}
diff --git a/test/js/third_party/socket.io/socket.io-close.test.ts b/test/js/third_party/socket.io/socket.io-close.test.ts
new file mode 100644
index 000000000..23b9277c1
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-close.test.ts
@@ -0,0 +1,216 @@
+import { describe, it, expect } from "bun:test";
+import { io as ioc } from "socket.io-client";
+import { join } from "path";
+import { createServer } from "http";
+import { createClient, getPort, success, fail, eioHandshake, eioPoll, eioPush } from "./support/util.ts";
+import { Server } from "socket.io";
+import { exec, ChildProcess } from "child_process";
+
+describe("close", () => {
+ it.skip("should be able to close sio sending a srv", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer);
+ const port = getPort(io);
+ const net = require("net");
+ const server = net.createServer();
+
+ const clientSocket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ clientSocket.on("disconnect", () => {
+ try {
+ expect(io.sockets.sockets.size).toBe(0);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ server.listen(port);
+ });
+
+ clientSocket.on("connect", () => {
+ try {
+ expect(io.sockets.sockets.size).toBe(1);
+ io.close();
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ server.once("listening", () => {
+ // PORT should be free
+ server.close((error: any) => {
+ clearTimeout(timeout);
+ try {
+ expect(error).toBe(undefined);
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+ });
+
+ it.skip("should be able to close sio sending a srv", done => {
+ const io = new Server(0);
+ const port = getPort(io);
+ const net = require("net");
+ const server = net.createServer();
+
+ const clientSocket = ioc("ws://0.0.0.0:" + port, {
+ reconnection: false,
+ });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ clientSocket.on("disconnect", () => {
+ try {
+ expect(io.sockets.sockets.size).toBe(0);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ server.listen(port);
+ });
+
+ clientSocket.on("connect", () => {
+ try {
+ expect(io.sockets.sockets.size).toBe(1);
+ io.close();
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ server.once("listening", () => {
+ // PORT should be free
+ server.close((error: any) => {
+ clearTimeout(timeout);
+ try {
+ expect(error).toBe(undefined);
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+ });
+
+ describe("graceful close", () => {
+ function fixture(filename: string) {
+ return '"' + process.execPath + '" "' + join(__dirname, "fixtures", filename) + '"';
+ }
+
+ it("should stop socket and timers", done => {
+ let process: ChildProcess;
+ const timeout = setTimeout(() => {
+ process?.kill();
+ done(new Error("timeout"));
+ }, 3000);
+
+ process = exec(fixture("server-close.ts"), err => {
+ clearTimeout(timeout);
+ done(err);
+ });
+ });
+ });
+
+ describe("protocol violations", () => {
+ it.skip("should close the connection when receiving several CONNECT packets", done => {
+ const httpServer = createServer();
+ const io = new Server(httpServer);
+
+ httpServer.listen(0);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 1500);
+
+ (async () => {
+ const sid = await eioHandshake(httpServer);
+ // send a first CONNECT packet
+ await eioPush(httpServer, sid, "40");
+ // send another CONNECT packet
+ await eioPush(httpServer, sid, "40");
+ // session is cleanly closed (not discarded, see 'client.close()')
+ // first, we receive the Socket.IO handshake response
+ await eioPoll(httpServer, sid);
+ // then a close packet
+ return await eioPoll(httpServer, sid);
+ })().then(body => {
+ clearTimeout(timeout);
+ try {
+ expect(body).toBe("6\u001e1");
+
+ io.close();
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ // TODO: IOT instruction can happen here
+ it.skip("should close the connection when receiving an EVENT packet while not connected", done => {
+ const httpServer = createServer();
+ const io = new Server(httpServer);
+
+ httpServer.listen(0);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 1500);
+
+ (async () => {
+ const sid = await eioHandshake(httpServer);
+ // send an EVENT packet
+ await eioPush(httpServer, sid, '42["some event"]');
+ // session is cleanly closed, we receive a close packet
+ return await eioPoll(httpServer, sid);
+ })().then(body => {
+ clearTimeout(timeout);
+ try {
+ expect(body).toBe("6\u001e1");
+
+ io.close();
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ // TODO: investigatye IOT instruction
+ it.skip("should close the connection when receiving an invalid packet", done => {
+ const httpServer = createServer();
+ const io = new Server(httpServer);
+
+ httpServer.listen(0);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 1500);
+
+ (async () => {
+ const sid = await eioHandshake(httpServer);
+ // send a CONNECT packet
+ await eioPush(httpServer, sid, "40");
+ // send an invalid packet
+ await eioPush(httpServer, sid, "4abc");
+ // session is cleanly closed (not discarded, see 'client.close()')
+ // first, we receive the Socket.IO handshake response
+ await eioPoll(httpServer, sid);
+ // then a close packet
+ return await eioPoll(httpServer, sid);
+ })().then(body => {
+ clearTimeout(timeout);
+ try {
+ expect(body).toBe("6\u001e1");
+
+ io.close();
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts b/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts
new file mode 100644
index 000000000..fbf7b2035
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts
@@ -0,0 +1,319 @@
+import { describe, it, expect } from "bun:test";
+
+import { Server, Socket } from "socket.io";
+import { waitFor, eioHandshake, eioPush, eioPoll, fail, success } from "./support/util.ts";
+import { createServer, Server as HttpServer } from "http";
+import { Adapter } from "socket.io-adapter";
+
+async function init(httpServer: HttpServer, io: Server) {
+ // Engine.IO handshake
+ const sid = await eioHandshake(httpServer);
+
+ // Socket.IO handshake
+ await eioPush(httpServer, sid, "40");
+ const handshakeBody = await eioPoll(httpServer, sid);
+
+ expect(handshakeBody.startsWith("40")).toBe(true);
+
+ const handshake = JSON.parse(handshakeBody.substring(2));
+
+ expect(handshake.sid).not.toBe(undefined);
+ // in that case, the handshake also contains a private session ID
+ expect(handshake.pid).not.toBe(undefined);
+
+ io.emit("hello");
+
+ const message = await eioPoll(httpServer, sid);
+
+ expect(message.startsWith('42["hello"')).toBe(true);
+
+ const offset = JSON.parse(message.substring(2))[1];
+ // in that case, each packet also includes an offset in the data array
+ expect(offset).not.toBe(undefined);
+
+ await eioPush(httpServer, sid, "1");
+ return [handshake.sid, handshake.pid, offset];
+}
+
+describe("connection state recovery", () => {
+ it.skip("should restore session and missed packets", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer, {
+ connectionStateRecovery: {},
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ let serverSocket: Socket | undefined;
+
+ io.once("connection", socket => {
+ socket.join("room1");
+ serverSocket = socket;
+ });
+
+ (async () => {
+ try {
+ const [sid, pid, offset] = await init(httpServer, io);
+
+ io.emit("hello1"); // broadcast
+ io.to("room1").emit("hello2"); // broadcast to room
+ serverSocket?.emit("hello3"); // direct message
+
+ const newSid = await eioHandshake(httpServer);
+ await eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`);
+
+ const payload = await eioPoll(httpServer, newSid);
+ clearTimeout(timeout);
+
+ const packets = payload.split("\x1e");
+
+ expect(packets.length).toBe(4);
+
+ // note: EVENT packets are received before the CONNECT packet, which is a bit weird
+ // see also: https://github.com/socketio/socket.io-deno/commit/518f534e1c205b746b1cb21fe76b187dabc96f34
+ expect(packets[0].startsWith('42["hello1"')).toBe(true);
+ expect(packets[1].startsWith('42["hello2"')).toBe(true);
+ expect(packets[2].startsWith('42["hello3"')).toBe(true);
+ expect(packets[3]).toBe(`40{"sid":"${sid}","pid":"${pid}"}`);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+
+ it.skip("should restore rooms and data attributes", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer, {
+ connectionStateRecovery: {},
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ io.once("connection", socket => {
+ expect(socket.recovered).toBe(false);
+
+ socket.join("room1");
+ socket.join("room2");
+ socket.data.foo = "bar";
+ });
+ (async () => {
+ try {
+ const [sid, pid, offset] = await init(httpServer, io);
+
+ const newSid = await eioHandshake(httpServer);
+
+ const [socket] = await Promise.all([
+ waitFor<Socket>(io, "connection"),
+ eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`),
+ ]);
+
+ clearTimeout(timeout);
+ expect(socket.id).toBe(sid);
+ expect(socket.recovered).toBe(true);
+
+ expect(socket.rooms.has(socket.id)).toBe(true);
+ expect(socket.rooms.has("room1")).toBe(true);
+ expect(socket.rooms.has("room2")).toBe(true);
+
+ expect(socket.data.foo).toBe("bar");
+
+ await eioPoll(httpServer, newSid); // drain buffer
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+
+ it.skip("should not run middlewares upon recovery by default", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer, {
+ connectionStateRecovery: {},
+ });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ (async () => {
+ try {
+ const [_, pid, offset] = await init(httpServer, io);
+
+ io.use((socket, next) => {
+ socket.data.middlewareWasCalled = true;
+
+ next();
+ });
+
+ const newSid = await eioHandshake(httpServer);
+
+ const [socket] = await Promise.all([
+ waitFor<Socket>(io, "connection"),
+ eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`),
+ ]);
+
+ clearTimeout(timeout);
+ expect(socket.recovered).toBe(true);
+ expect(socket.data.middlewareWasCalled).toBe(undefined);
+
+ await eioPoll(httpServer, newSid); // drain buffer
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+
+ it.skip("should run middlewares even upon recovery", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer, {
+ connectionStateRecovery: {
+ skipMiddlewares: false,
+ },
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ (async () => {
+ try {
+ const [_, pid, offset] = await init(httpServer, io);
+
+ io.use((socket, next) => {
+ socket.data.middlewareWasCalled = true;
+
+ next();
+ });
+
+ const newSid = await eioHandshake(httpServer);
+
+ const [socket] = await Promise.all([
+ waitFor<Socket>(io, "connection"),
+ eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`),
+ ]);
+
+ clearTimeout(timeout);
+
+ expect(socket.recovered).toBe(true);
+ expect(socket.data.middlewareWasCalled).toBe(true);
+
+ await eioPoll(httpServer, newSid); // drain buffer
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+
+ it.skip("should fail to restore an unknown session", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer, {
+ connectionStateRecovery: {},
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ (async () => {
+ try {
+ // Engine.IO handshake
+ const sid = await eioHandshake(httpServer);
+
+ // Socket.IO handshake
+ await eioPush(httpServer, sid, '40{"pid":"foo","offset":"bar"}');
+
+ const handshakeBody = await eioPoll(httpServer, sid);
+
+ clearTimeout(timeout);
+
+ expect(handshakeBody.startsWith("40")).toBe(true);
+
+ const handshake = JSON.parse(handshakeBody.substring(2));
+
+ expect(handshake.sid).not.toBe("foo");
+ expect(handshake.pid).not.toBe("bar");
+
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+
+ it("should be disabled by default", done => {
+ const httpServer = createServer().listen(0);
+ const io = new Server(httpServer);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ (async () => {
+ try {
+ // Engine.IO handshake
+ const sid = await eioHandshake(httpServer);
+
+ // Socket.IO handshake
+ await eioPush(httpServer, sid, "40");
+
+ const handshakeBody = await eioPoll(httpServer, sid);
+
+ clearTimeout(timeout);
+ expect(handshakeBody.startsWith("40")).toBe(true);
+
+ const handshake = JSON.parse(handshakeBody.substring(2));
+
+ expect(handshake.sid).not.toBe(undefined);
+ expect(handshake.pid).toBe(undefined);
+
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+
+ it("should not call adapter#persistSession or adapter#restoreSession if disabled", done => {
+ const httpServer = createServer().listen(0);
+
+ let io: Server;
+ class DummyAdapter extends Adapter {
+ override persistSession() {
+ fail(done, io, new Error("should not happen"));
+ return Promise.reject("should not happen");
+ }
+
+ override restoreSession() {
+ fail(done, io, new Error("should not happen"));
+ return Promise.reject("should not happen");
+ }
+ }
+
+ io = new Server(httpServer, {
+ adapter: DummyAdapter,
+ });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ (async () => {
+ try {
+ // Engine.IO handshake
+ const sid = await eioHandshake(httpServer);
+
+ await eioPush(httpServer, sid, '40{"pid":"foo","offset":"bar"}');
+ await eioPoll(httpServer, sid);
+ await eioPush(httpServer, sid, "1");
+ clearTimeout(timeout);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ })();
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-handshake.test.ts b/test/js/third_party/socket.io/socket.io-handshake.test.ts
new file mode 100644
index 000000000..e29808080
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-handshake.test.ts
@@ -0,0 +1,118 @@
+import { Server } from "socket.io";
+import { describe, it, expect } from "bun:test";
+import { getPort, success, fail } from "./support/util.ts";
+
+describe("handshake", () => {
+ const request = require("superagent");
+
+ it("should send the Access-Control-Allow-xxx headers on OPTIONS request", done => {
+ const io = new Server(0, {
+ cors: {
+ origin: "http://localhost:54023",
+ methods: ["GET", "POST"],
+ allowedHeaders: ["content-type"],
+ credentials: true,
+ },
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ request
+ .options(`http://localhost:${getPort(io)}/socket.io/default/`)
+ .query({ transport: "polling", EIO: 4 })
+ .set("Origin", "http://localhost:54023")
+ .end((err, res) => {
+ try {
+ clearTimeout(timeout);
+ expect(res.status).toBe(204);
+
+ expect(res.headers["access-control-allow-origin"]).toBe("http://localhost:54023");
+ expect(res.headers["access-control-allow-methods"]).toBe("GET,POST");
+ expect(res.headers["access-control-allow-headers"]).toBe("content-type");
+ expect(res.headers["access-control-allow-credentials"]).toBe("true");
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ it("should send the Access-Control-Allow-xxx headers on GET request", done => {
+ const io = new Server(0, {
+ cors: {
+ origin: "http://localhost:54024",
+ methods: ["GET", "POST"],
+ allowedHeaders: ["content-type"],
+ credentials: true,
+ },
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ request
+ .get(`http://localhost:${getPort(io)}/socket.io/default/`)
+ .query({ transport: "polling", EIO: 4 })
+ .set("Origin", "http://localhost:54024")
+ .end((err, res) => {
+ clearTimeout(timeout);
+ try {
+ expect(res.status).toBe(200);
+
+ expect(res.headers["access-control-allow-origin"]).toBe("http://localhost:54024");
+ expect(res.headers["access-control-allow-credentials"]).toBe("true");
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ it("should allow request if custom function in opts.allowRequest returns true", done => {
+ const io = new Server(0, {
+ allowRequest: (req, callback) => callback(null, true),
+ });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ request
+ .get(`http://localhost:${getPort(io)}/socket.io/default/`)
+ .query({ transport: "polling", EIO: 4 })
+ .end((err, res) => {
+ try {
+ clearTimeout(timeout);
+ expect(res.status).toBe(200);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ it("should disallow request if custom function in opts.allowRequest returns false", done => {
+ const io = new Server(0, {
+ allowRequest: (req, callback) => callback(null, false),
+ });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+ request
+ .get(`http://localhost:${getPort(io)}/socket.io/default/`)
+ .set("origin", "http://foo.example")
+ .query({ transport: "polling", EIO: 4 })
+ .end((err, res) => {
+ try {
+ clearTimeout(timeout);
+ expect(res.status).toBe(403);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-messaging-many.test.ts b/test/js/third_party/socket.io/socket.io-messaging-many.test.ts
new file mode 100644
index 000000000..44b80928c
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-messaging-many.test.ts
@@ -0,0 +1,766 @@
+import { Server } from "socket.io";
+import { describe, it, expect } from "bun:test";
+import { createClient, createPartialDone, success, fail, waitFor } from "./support/util";
+
+describe("messaging many", () => {
+ it("emits to a namespace", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/test");
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err, socket1, socket2, socket3);
+ success(done, io, socket1, socket2, socket3);
+ });
+
+ socket1.on("a", a => {
+ try {
+ expect(a).toBe("b");
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ socket2.on("a", a => {
+ try {
+ expect(a).toBe("b");
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ socket3.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2, socket3);
+ });
+
+ let sockets = 3;
+ io.on("connection", () => {
+ --sockets || emit();
+ });
+ io.of("/test", () => {
+ --sockets || emit();
+ });
+
+ function emit() {
+ io.emit("a", "b");
+ }
+ });
+
+ it("emits binary data to a namespace", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/test");
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err, socket1, socket2, socket3);
+ success(done, io, socket1, socket2, socket3);
+ });
+
+ socket1.on("bin", a => {
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ socket2.on("bin", a => {
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ socket3.on("bin", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2, socket3);
+ });
+
+ let sockets = 3;
+ io.on("connection", () => {
+ --sockets || emit();
+ });
+ io.of("/test", () => {
+ --sockets || emit();
+ });
+
+ function emit() {
+ io.emit("bin", Buffer.alloc(10));
+ }
+ });
+
+ it("emits to the rest", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/test");
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ socket1.on("a", a => {
+ try {
+ expect(a).toBe("b");
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ socket1.emit("finish");
+ });
+ socket2.emit("broadcast");
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("done"), socket1, socket2, socket3);
+ });
+ socket3.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2, socket3);
+ });
+
+ io.on("connection", socket => {
+ socket.on("broadcast", () => {
+ socket.broadcast.emit("a", "b");
+ });
+ socket.on("finish", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2, socket3);
+ });
+ });
+ });
+
+ it("emits to rooms", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2);
+ }, 200);
+
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2);
+ });
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2);
+ });
+ socket1.emit("join", "woot");
+ socket1.emit("emit", "woot");
+
+ io.on("connection", socket => {
+ socket.on("join", (room, fn) => {
+ socket.join(room);
+ fn && fn();
+ });
+
+ socket.on("emit", room => {
+ io.in(room).emit("a");
+ });
+ });
+ });
+
+ it("emits to rooms avoiding dupes", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2);
+ }, 200);
+
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err, socket1, socket2);
+ success(done, io, socket1, socket2);
+ });
+
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2);
+ });
+ socket1.on("a", partialDone);
+ socket2.on("b", partialDone);
+
+ socket1.emit("join", "woot");
+ socket1.emit("join", "test");
+ socket2.emit("join", "third", () => {
+ socket2.emit("emit");
+ });
+
+ io.on("connection", socket => {
+ socket.on("join", (room, fn) => {
+ socket.join(room);
+ fn && fn();
+ });
+
+ socket.on("emit", () => {
+ io.in("woot").in("test").emit("a");
+ io.in("third").emit("b");
+ });
+ });
+ });
+
+ it("broadcasts to rooms", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err, socket1, socket2, socket3);
+ success(done, io, socket1, socket2);
+ });
+
+ socket1.emit("join", "woot");
+ socket2.emit("join", "test");
+ socket3.emit("join", "test", () => {
+ socket3.emit("broadcast");
+ });
+
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2, socket3);
+ });
+ socket2.on("a", () => {
+ partialDone();
+ });
+ socket3.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("not"), socket1, socket2, socket3);
+ });
+ socket3.on("b", () => {
+ partialDone();
+ });
+
+ io.on("connection", socket => {
+ socket.on("join", (room, fn) => {
+ socket.join(room);
+ fn && fn();
+ });
+
+ socket.on("broadcast", () => {
+ socket.broadcast.to("test").emit("a");
+ socket.emit("b");
+ });
+ });
+ });
+
+ it("broadcasts binary data to rooms", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err, socket1, socket2, socket3);
+ success(done, io, socket1, socket2);
+ });
+
+ socket1.emit("join", "woot");
+ socket2.emit("join", "test");
+ socket3.emit("join", "test", () => {
+ socket3.emit("broadcast");
+ });
+
+ socket1.on("bin", data => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("got bin in socket1"), socket1, socket2, socket3);
+ });
+ socket2.on("bin", data => {
+ try {
+ expect(Buffer.isBuffer(data)).toBe(true);
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ socket2.on("bin2", data => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("socket2 got bin2"), socket1, socket2, socket3);
+ });
+ socket3.on("bin", data => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("socket3 got bin"), socket1, socket2, socket3);
+ });
+ socket3.on("bin2", data => {
+ try {
+ expect(Buffer.isBuffer(data)).toBe(true);
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+
+ io.on("connection", socket => {
+ socket.on("join", (room, fn) => {
+ socket.join(room);
+ fn && fn();
+ });
+ socket.on("broadcast", () => {
+ socket.broadcast.to("test").emit("bin", Buffer.alloc(5));
+ socket.emit("bin2", Buffer.alloc(5));
+ });
+ });
+ });
+
+ it("keeps track of rooms", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ s.join("a");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a"]));
+ s.join("b");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a", "b"]));
+ s.join("c");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a", "b", "c"]));
+ s.leave("b");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a", "c"]));
+ (s as any).leaveAll();
+ expect(s.rooms.size).toBe(0);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("deletes empty rooms", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ s.join("a");
+ expect(s.nsp.adapter.rooms.has("a")).toBe(true);
+ s.leave("a");
+ expect(s.nsp.adapter.rooms.has("a")).toBe(false);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should properly cleanup left rooms", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ s.join("a");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a"]));
+ s.join("b");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a", "b"]));
+ s.leave("unknown");
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a", "b"]));
+ (s as any).leaveAll();
+ expect(s.rooms.size).toBe(0);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("allows to join several rooms at once", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ s.join(["a", "b", "c"]);
+
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a", "b", "c"]));
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should exclude specific sockets when broadcasting", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("socket2 got a"), socket1, socket2, socket3);
+ });
+ socket3.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("socket3 got a"), socket1, socket2, socket3);
+ });
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2, socket3);
+ });
+
+ io.on("connection", socket => {
+ socket.on("exclude", id => {
+ socket.broadcast.except(id).emit("a");
+ });
+ });
+
+ socket2.on("connect", () => {
+ socket3.emit("exclude", socket2.id);
+ });
+ });
+
+ it("should exclude a specific room when broadcasting", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 200);
+
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("socket2 got a"), socket1, socket2, socket3);
+ });
+ socket3.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("socket3 got a"), socket1, socket2, socket3);
+ });
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2, socket3);
+ });
+
+ io.on("connection", socket => {
+ socket.on("join", (room, cb) => {
+ socket.join(room);
+ cb();
+ });
+ socket.on("broadcast", () => {
+ socket.broadcast.except("room1").emit("a");
+ });
+ });
+
+ socket2.emit("join", "room1", () => {
+ socket3.emit("broadcast");
+ });
+ });
+
+ it("should return an immutable broadcast operator", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ io.on("connection", socket => {
+ clearTimeout(timeout);
+ try {
+ const operator = socket.local.compress(false).to(["room1", "room2"]).except("room3");
+ operator.compress(true).emit("hello");
+ operator.volatile.emit("hello");
+ operator.to("room4").emit("hello");
+ operator.except("room5").emit("hello");
+ socket.emit("hello");
+ socket.to("room6").emit("hello");
+ // @ts-ignore
+ expect(operator.rooms).toStrictEqual(new Set(["room1", "room2"]));
+ // @ts-ignore
+ expect(operator.rooms.has("room4")).toBeFalsy();
+ // @ts-ignore
+ expect(operator.rooms.has("room5")).toBeFalsy();
+ // @ts-ignore
+ expect(operator.rooms.has("room6")).toBeFalsy();
+ // @ts-ignore
+ expect(operator.exceptRooms.has("room3")).toBe(true);
+ // @ts-ignore
+ expect(operator.flags).toStrictEqual({ local: true, compress: false });
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+
+ it("should broadcast and expect multiple acknowledgements", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 3000);
+
+ socket1.on("some event", cb => {
+ cb(1);
+ });
+
+ socket2.on("some event", cb => {
+ cb(2);
+ });
+
+ socket3.on("some event", cb => {
+ cb(3);
+ });
+
+ Promise.all([waitFor(socket1, "connect"), waitFor(socket2, "connect"), waitFor(socket3, "connect")]).then(() => {
+ io.timeout(2000).emit("some event", (err, responses) => {
+ clearTimeout(timeout);
+ try {
+ expect(err).toBe(null);
+ expect(responses.length).toBe(3);
+ expect(responses).toContain(1);
+ expect(responses).toContain(2);
+ expect(responses).toContain(3);
+
+ success(done, io, socket1, socket2, socket3);
+ } catch (err) {
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ });
+ });
+
+ it("should fail when a client does not acknowledge the event in the given delay", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 300);
+
+ socket1.on("some event", cb => {
+ cb(1);
+ });
+
+ socket2.on("some event", cb => {
+ cb(2);
+ });
+
+ socket3.on("some event", () => {
+ // timeout
+ });
+
+ Promise.all([waitFor(socket1, "connect"), waitFor(socket2, "connect"), waitFor(socket3, "connect")]).then(() => {
+ io.timeout(200).emit("some event", (err, responses) => {
+ clearTimeout(timeout);
+ try {
+ expect(err).toBeInstanceOf(Error);
+ expect(responses.length).toBe(2);
+ expect(responses).toContain(1);
+ expect(responses).toContain(2);
+
+ success(done, io, socket1, socket2, socket3);
+ success(done, io, socket1, socket2, socket3);
+ } catch (err) {
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ });
+ });
+
+ it("should broadcast and expect multiple acknowledgements (promise)", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 3000);
+
+ socket1.on("some event", cb => {
+ cb(1);
+ });
+
+ socket2.on("some event", cb => {
+ cb(2);
+ });
+
+ socket3.on("some event", cb => {
+ cb(3);
+ });
+
+ Promise.all([waitFor(socket1, "connect"), waitFor(socket2, "connect"), waitFor(socket3, "connect")]).then(
+ async () => {
+ try {
+ const responses = await io.timeout(2000).emitWithAck("some event");
+ clearTimeout(timeout);
+ expect(responses).toContain(1);
+ expect(responses).toContain(2);
+ expect(responses).toContain(3);
+
+ success(done, io, socket1, socket2, socket3);
+ } catch (err) {
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ },
+ );
+ });
+
+ it("should fail when a client does not acknowledge the event in the given delay (promise)", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 300);
+
+ socket1.on("some event", cb => {
+ cb(1);
+ });
+
+ socket2.on("some event", cb => {
+ cb(2);
+ });
+
+ socket3.on("some event", () => {
+ // timeout
+ });
+
+ Promise.all([waitFor(socket1, "connect"), waitFor(socket2, "connect"), waitFor(socket3, "connect")]).then(
+ async () => {
+ try {
+ await io.timeout(200).emitWithAck("some event");
+ clearTimeout(timeout);
+ fail(done, io, new Error("should not happen"), socket1, socket2, socket3);
+ } catch (err) {
+ clearTimeout(timeout);
+ try {
+ expect(err).toBeInstanceOf(Error);
+ // @ts-ignore
+ expect(err.responses.length).toBe(2);
+ // @ts-ignore
+ expect(err.responses).toContain(1);
+ // @ts-ignore
+ expect(err.responses).toContain(2);
+
+ success(done, io, socket1, socket2, socket3);
+ } catch (err) {
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ }
+ },
+ );
+ });
+
+ it("should broadcast and return if the packet is sent to 0 client", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/", { multiplex: false });
+ const socket2 = createClient(io, "/", { multiplex: false });
+ const socket3 = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 300);
+
+ socket1.on("some event", () => {
+ done(new Error("should not happen"));
+ });
+
+ socket2.on("some event", () => {
+ done(new Error("should not happen"));
+ });
+
+ socket3.on("some event", () => {
+ done(new Error("should not happen"));
+ });
+
+ io.to("room123")
+ .timeout(200)
+ .emit("some event", (err, responses) => {
+ clearTimeout(timeout);
+ try {
+ expect(err).toBe(null);
+ expect(responses.length).toBe(0);
+
+ success(done, io, socket1, socket2, socket3);
+ } catch (err) {
+ fail(done, io, err, socket1, socket2, socket3);
+ }
+ });
+ });
+
+ it.skip("should precompute the WebSocket frame when broadcasting", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/chat", {
+ transports: ["websocket"],
+ });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ if (err) fail(done, io, err, socket);
+ else success(done, io, socket);
+ });
+
+ io.of("/chat").on("connection", s => {
+ s.conn.once("packetCreate", packet => {
+ try {
+ expect(packet.options.wsPreEncodedFrame).toBeInstanceOf(Array);
+ partialDone();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket);
+ }
+ });
+ io.of("/chat").compress(false).emit("woot", "hi");
+ });
+
+ socket.on("woot", partialDone);
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-middleware.test.ts b/test/js/third_party/socket.io/socket.io-middleware.test.ts
new file mode 100644
index 000000000..637d0e08f
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-middleware.test.ts
@@ -0,0 +1,299 @@
+import { Server, Socket } from "socket.io";
+import { describe, it, expect } from "bun:test";
+
+import { success, fail, createClient, createPartialDone } from "./support/util.ts";
+
+describe("middleware", () => {
+ it("should call functions", done => {
+ const io = new Server(0);
+ let timeout: Timer;
+
+ let run = 0;
+ io.use((socket, next) => {
+ try {
+ expect(socket).toBeInstanceOf(Socket);
+ run++;
+ next();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err);
+ }
+ });
+ io.use((socket, next) => {
+ try {
+ expect(socket).toBeInstanceOf(Socket);
+ run++;
+ next();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err);
+ }
+ });
+
+ const socket = createClient(io);
+ timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ socket.on("connect", () => {
+ try {
+ clearTimeout(timeout);
+ expect(run).toBe(2);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should pass errors", done => {
+ const io = new Server(0);
+ let timeout: Timer;
+ let socket;
+ io.use((socket, next) => {
+ next(new Error("Authentication error"));
+ });
+ io.use((socket, next) => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("nope"), socket);
+ });
+
+ socket = createClient(io);
+ timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ socket.on("connect", () => {
+ done(new Error("nope"));
+ });
+ socket.on("connect_error", err => {
+ try {
+ clearTimeout(timeout);
+ expect(err.message).toBe("Authentication error");
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should pass an object", done => {
+ const io = new Server(0);
+
+ io.use((socket, next) => {
+ const err = new Error("Authentication error");
+ // @ts-ignore
+ err.data = { a: "b", c: 3 };
+ next(err);
+ });
+
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ socket.on("connect", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("nope"), socket);
+ });
+
+ socket.on("connect_error", err => {
+ try {
+ clearTimeout(timeout);
+ expect(err).toBeInstanceOf(Error);
+ expect(err.message).toBe("Authentication error");
+ // @ts-ignore
+ expect(err.data).toStrictEqual({ a: "b", c: 3 });
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should only call connection after fns", done => {
+ const io = new Server(0);
+
+ io.use((socket: any, next) => {
+ socket.name = "guillermo";
+ next();
+ });
+
+ const clientSocket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ io.on("connection", socket => {
+ try {
+ clearTimeout(timeout);
+ expect((socket as any).name).toBe("guillermo");
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+
+ it("should only call connection after (lengthy) fns", done => {
+ const io = new Server(0);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 2000);
+
+ let authenticated = false;
+
+ io.use((socket, next) => {
+ setTimeout(() => {
+ authenticated = true;
+ next();
+ }, 300);
+ });
+
+ const socket = createClient(io);
+ socket.on("connect", () => {
+ try {
+ clearTimeout(timeout);
+ expect(authenticated).toBe(true);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should be ignored if socket gets closed", done => {
+ const io = new Server(0);
+ let timeout: Timer;
+ let socket;
+ io.use((s, next) => {
+ socket.io.engine.close();
+ s.client.conn.on("close", () => {
+ process.nextTick(next);
+ setTimeout(() => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ }, 50);
+ });
+ });
+
+ socket = createClient(io);
+ timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", socket => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("should not fire"), socket);
+ });
+ });
+
+ it("should call functions in expected order", done => {
+ const io = new Server(0);
+
+ const result: number[] = [];
+
+ io.use(() => {
+ fail(done, io, new Error("should not fire"));
+ });
+ io.of("/chat").use((socket, next) => {
+ result.push(1);
+ setTimeout(next, 50);
+ });
+ io.of("/chat").use((socket, next) => {
+ result.push(2);
+ setTimeout(next, 50);
+ });
+ io.of("/chat").use((socket, next) => {
+ result.push(3);
+ setTimeout(next, 50);
+ });
+
+ const chat = createClient(io, "/chat");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), chat);
+ }, 1000);
+
+ chat.on("connect", () => {
+ clearTimeout(timeout);
+ try {
+ expect(result).toStrictEqual([1, 2, 3]);
+
+ success(done, io, chat);
+ } catch (err) {
+ fail(done, io, err, chat);
+ }
+ });
+ });
+
+ it("should disable the merge of handshake packets", done => {
+ const io = new Server(0);
+ io.use((socket, next) => {
+ next();
+ });
+
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ socket.on("connect", () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+
+ it("should work with a custom namespace", done => {
+ const io = new Server(0);
+ const socket1 = createClient(io, "/");
+ const socket2 = createClient(io, "/chat");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2);
+ }, 200);
+
+ const partialDone = createPartialDone(2, () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2);
+ });
+
+ io.of("/chat").use((socket, next) => {
+ next();
+ });
+
+ socket1.on("connect", partialDone);
+ socket2.on("connect", partialDone);
+ });
+
+ it("should only set `connected` to true after the middleware execution", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+ io.use((socket, next) => {
+ try {
+ expect(socket.connected).toBe(false);
+ expect(socket.disconnected).toBe(true);
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, clientSocket);
+ }
+ next();
+ });
+
+ io.on("connection", socket => {
+ try {
+ clearTimeout(timeout);
+ expect(socket.connected).toBe(true);
+ expect(socket.disconnected).toBe(false);
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-namespaces.test.ts b/test/js/third_party/socket.io/socket.io-namespaces.test.ts
new file mode 100644
index 000000000..87df24a57
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-namespaces.test.ts
@@ -0,0 +1,925 @@
+import { describe, it, expect } from "bun:test";
+import type { SocketId } from "socket.io-adapter";
+import { Server, Namespace, Socket } from "socket.io";
+import { success, fail, createClient, createPartialDone } from "./support/util.ts";
+
+describe("namespaces", () => {
+ it("should be accessible through .sockets", done => {
+ const io = new Server();
+ expect(io.sockets).toBeInstanceOf(Namespace);
+ done();
+ });
+
+ it("should be aliased", done => {
+ const io = new Server();
+ expect(typeof io.use).toBe("function");
+ expect(typeof io.to).toBe("function");
+ expect(typeof io["in"]).toBe("function");
+ expect(typeof io.emit).toBe("function");
+ expect(typeof io.send).toBe("function");
+ expect(typeof io.write).toBe("function");
+ expect(typeof io.allSockets).toBe("function");
+ expect(typeof io.compress).toBe("function");
+ done();
+ });
+
+ it("should return an immutable broadcast operator", done => {
+ const io = new Server();
+ const operator = io.local.to(["room1", "room2"]).except("room3");
+ operator.compress(true).emit("hello");
+ operator.volatile.emit("hello");
+ operator.to("room4").emit("hello");
+ operator.except("room5").emit("hello");
+ io.to("room6").emit("hello");
+ // @ts-ignore
+ expect(operator.rooms).toStrictEqual(new Set(["room1", "room2"]));
+ // @ts-ignore
+ expect(operator.exceptRooms).toStrictEqual(new Set(["room3"]));
+ // @ts-ignore
+ expect(operator.flags).toStrictEqual({ local: true });
+
+ done();
+ });
+
+ it("should automatically connect", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ socket.on("connect", () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+
+ it("should fire a `connection` event", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ expect(s).toBeInstanceOf(Socket);
+ success(done, io, socket);
+ });
+ });
+
+ it("should fire a `connect` event", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+ io.on("connect", s => {
+ clearTimeout(timeout);
+ expect(s).toBeInstanceOf(Socket);
+ success(done, io, socket);
+ });
+ });
+
+ it("should work with many sockets", done => {
+ const io = new Server(0);
+ io.of("/chat");
+ io.of("/news");
+ const chat = createClient(io, "/chat");
+ const news = createClient(io, "/news");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), chat, news);
+ }, 300);
+
+ let total = 2;
+ function _success() {
+ clearTimeout(timeout);
+ success(done, io, chat, news);
+ }
+
+ chat.on("connect", () => {
+ --total || _success();
+ });
+ news.on("connect", () => {
+ --total || _success();
+ });
+ });
+
+ it('should be able to equivalently start with "" or "/" on server', done => {
+ const io = new Server(0);
+ const c1 = createClient(io, "/");
+ const c2 = createClient(io, "/abc");
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1, c2);
+ }, 300);
+
+ let total = 2;
+ function _success() {
+ clearTimeout(timeout);
+ success(done, io, c1, c2);
+ }
+
+ io.of("").on("connection", () => {
+ --total || _success();
+ });
+ io.of("abc").on("connection", () => {
+ --total || _success();
+ });
+ });
+
+ it('should be equivalent for "" and "/" on client', done => {
+ const io = new Server(0);
+ const c1 = createClient(io, "");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1);
+ }, 300);
+ io.of("/").on("connection", () => {
+ clearTimeout(timeout);
+ success(done, io, c1);
+ });
+ });
+
+ it("should work with `of` and many sockets", done => {
+ const io = new Server(0);
+ const chat = createClient(io, "/chat");
+ const news = createClient(io, "/news");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), chat, news);
+ }, 300);
+
+ let total = 2;
+ function _success() {
+ clearTimeout(timeout);
+ success(done, io, chat, news);
+ }
+ io.of("/news").on("connection", socket => {
+ try {
+ expect(socket).toBeInstanceOf(Socket);
+ --total || _success();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, chat, news);
+ }
+ });
+ io.of("/news").on("connection", socket => {
+ try {
+ expect(socket).toBeInstanceOf(Socket);
+ --total || _success();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, chat, news);
+ }
+ });
+ });
+
+ it("should work with `of` second param", done => {
+ const io = new Server(0);
+ const chat = createClient(io, "/chat");
+ const news = createClient(io, "/news");
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), chat, news);
+ }, 300);
+
+ let total = 2;
+ function _success() {
+ clearTimeout(timeout);
+ success(done, io, chat, news);
+ }
+ io.of("/news", socket => {
+ try {
+ expect(socket).toBeInstanceOf(Socket);
+ --total || _success();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, chat, news);
+ }
+ });
+ io.of("/news", socket => {
+ try {
+ expect(socket).toBeInstanceOf(Socket);
+ --total || _success();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, chat, news);
+ }
+ });
+ });
+
+ it("should disconnect upon transport disconnection", done => {
+ const io = new Server(0);
+ const chat = createClient(io, "/chat");
+ const news = createClient(io, "/news");
+
+ let total = 2;
+ let totald = 2;
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), chat, news);
+ }, 300);
+
+ function _success() {
+ clearTimeout(timeout);
+ success(done, io, chat, news);
+ }
+
+ function close() {
+ s.disconnect(true);
+ }
+
+ let s: Socket;
+ io.of("/news", socket => {
+ socket.on("disconnect", reason => {
+ --totald || _success();
+ });
+ --total || close();
+ });
+ io.of("/chat", socket => {
+ s = socket;
+ socket.on("disconnect", reason => {
+ --totald || _success();
+ });
+ --total || close();
+ });
+ });
+
+ it("should fire a `disconnecting` event just before leaving all rooms", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", s => {
+ s.join("a");
+ // FIXME not sure why process.nextTick() is needed here
+ process.nextTick(() => s.disconnect());
+
+ let total = 2;
+
+ function _success() {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ }
+
+ s.on("disconnecting", reason => {
+ try {
+ expect(s.rooms).toStrictEqual(new Set([s.id, "a"]));
+ total--;
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket);
+ }
+ });
+
+ s.on("disconnect", reason => {
+ try {
+ expect(s.rooms.size).toBe(0);
+ --total || _success();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, socket);
+ }
+ });
+ });
+ });
+
+ it("should return error connecting to non-existent namespace", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/doesnotexist");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ socket.on("connect_error", err => {
+ clearTimeout(timeout);
+ try {
+ expect(err.message).toBe("Invalid namespace");
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should not reuse same-namespace connections", done => {
+ const io = new Server(0);
+ const clientSocket1 = createClient(io);
+ const clientSocket2 = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket1, clientSocket2);
+ }, 300);
+
+ let connections = 0;
+ io.on("connection", () => {
+ connections++;
+ if (connections === 2) {
+ clearTimeout(timeout);
+ success(done, io, clientSocket1, clientSocket2);
+ }
+ });
+ });
+
+ it("should find all clients in a namespace", done => {
+ const io = new Server(0);
+ const chatSids: string[] = [];
+ let otherSid: SocketId | null = null;
+
+ const c1 = createClient(io, "/chat");
+ const c2 = createClient(io, "/chat", { forceNew: true });
+ const c3 = createClient(io, "/other", { forceNew: true });
+
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1, c2, c3);
+ }, 300);
+
+ let total = 3;
+ io.of("/chat").on("connection", socket => {
+ chatSids.push(socket.id);
+ --total || getSockets();
+ });
+ io.of("/other").on("connection", socket => {
+ otherSid = socket.id;
+ --total || getSockets();
+ });
+
+ async function getSockets() {
+ const sids = await io.of("/chat").allSockets();
+ clearTimeout(timeout);
+ try {
+ expect(sids).toStrictEqual(new Set([chatSids[0], chatSids[1]]));
+ expect(sids).not.toContain(otherSid);
+ success(done, io, c1, c2, c3);
+ } catch (err) {
+ fail(done, io, err, c1, c2, c3);
+ }
+ }
+ });
+
+ it("should find all clients in a namespace room", done => {
+ const io = new Server(0);
+ let chatFooSid: SocketId | null = null;
+ let chatBarSid: SocketId | null = null;
+ let otherSid: SocketId | null = null;
+
+ const c1 = createClient(io, "/chat");
+ const c2 = createClient(io, "/chat", { forceNew: true });
+ const c3 = createClient(io, "/other", { forceNew: true });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1, c2, c3);
+ }, 300);
+
+ let chatIndex = 0;
+ let total = 3;
+ io.of("/chat").on("connection", socket => {
+ if (chatIndex++) {
+ socket.join("foo");
+ chatFooSid = socket.id;
+ --total || getSockets();
+ } else {
+ socket.join("bar");
+ chatBarSid = socket.id;
+ --total || getSockets();
+ }
+ });
+ io.of("/other").on("connection", socket => {
+ socket.join("foo");
+ otherSid = socket.id;
+ --total || getSockets();
+ });
+
+ async function getSockets() {
+ const sids = await io.of("/chat").in("foo").allSockets();
+ clearTimeout(timeout);
+ try {
+ expect(sids).toStrictEqual(new Set([chatFooSid]));
+ expect(sids).not.toContain(chatBarSid);
+ expect(sids).not.toContain(otherSid);
+ success(done, io, c1, c2, c3);
+ } catch (err) {
+ fail(done, io, err, c1, c2, c3);
+ }
+ }
+ });
+
+ it("should find all clients across namespace rooms", done => {
+ const io = new Server(0);
+ let chatFooSid: SocketId | null = null;
+ let chatBarSid: SocketId | null = null;
+ let otherSid: SocketId | null = null;
+
+ const c1 = createClient(io, "/chat");
+ const c2 = createClient(io, "/chat", { forceNew: true });
+ const c3 = createClient(io, "/other", { forceNew: true });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1, c2, c3);
+ }, 300);
+
+ let chatIndex = 0;
+ let total = 3;
+ io.of("/chat").on("connection", socket => {
+ if (chatIndex++) {
+ socket.join("foo");
+ chatFooSid = socket.id;
+ --total || getSockets();
+ } else {
+ socket.join("bar");
+ chatBarSid = socket.id;
+ --total || getSockets();
+ }
+ });
+ io.of("/other").on("connection", socket => {
+ socket.join("foo");
+ otherSid = socket.id;
+ --total || getSockets();
+ });
+
+ async function getSockets() {
+ const sids = await io.of("/chat").allSockets();
+ clearTimeout(timeout);
+ try {
+ expect(sids).toStrictEqual(new Set([chatFooSid, chatBarSid]));
+ expect(sids).not.toContain(otherSid);
+ success(done, io, c1, c2, c3);
+ } catch (err) {
+ fail(done, io, err, c1, c2, c3);
+ }
+ }
+ });
+
+ it("should not emit volatile event after regular event", done => {
+ const io = new Server(0);
+
+ let counter = 0;
+ io.of("/chat").on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ io.of("/chat").emit("ev", "data");
+ io.of("/chat").volatile.emit("ev", "data");
+ }, 50);
+ });
+
+ const socket = createClient(io, "/chat");
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 300);
+ });
+
+ it("should emit volatile event", done => {
+ const io = new Server(0);
+
+ let counter = 0;
+ io.of("/chat").on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ io.of("/chat").volatile.emit("ev", "data");
+ }, 100);
+ });
+
+ const socket = createClient(io, "/chat");
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 300);
+ });
+
+ it("should enable compression by default", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/chat");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.of("/chat").on("connection", s => {
+ s.conn.once("packetCreate", packet => {
+ clearTimeout(timeout);
+ try {
+ expect(packet.options.compress).toBe(true);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.of("/chat").emit("woot", "hi");
+ });
+ });
+
+ it("should disable compression", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/chat");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.of("/chat").on("connection", s => {
+ s.conn.once("packetCreate", packet => {
+ clearTimeout(timeout);
+ try {
+ expect(packet.options.compress).toBe(false);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.of("/chat").compress(false).emit("woot", "hi");
+ });
+ });
+
+ it("should throw on reserved event", () => {
+ const io = new Server();
+
+ expect(() => io.emit("connect")).toThrow(/"connect" is a reserved event name/);
+ });
+
+ it("should close a client without namespace", done => {
+ const io = new Server(0, {
+ connectTimeout: 10,
+ });
+
+ const socket = createClient(io);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ // @ts-ignore
+ socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet
+
+ socket.on("disconnect", () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+
+ it("should exclude a specific socket when emitting", done => {
+ const io = new Server(0);
+
+ const socket1 = createClient(io, "/");
+ const socket2 = createClient(io, "/");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2);
+ }, 300);
+
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("should not happen"), socket1, socket2);
+ });
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2);
+ });
+
+ socket2.on("connect", () => {
+ io.except(socket2.id).emit("a");
+ });
+ });
+
+ it("should exclude a specific socket when emitting (in a namespace)", done => {
+ const io = new Server(0);
+
+ const nsp = io.of("/nsp");
+
+ const socket1 = createClient(io, "/nsp");
+ const socket2 = createClient(io, "/nsp");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2);
+ }, 300);
+
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("should not happen"), socket1, socket2);
+ });
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2);
+ });
+
+ socket2.on("connect", () => {
+ nsp.except(socket2.id).emit("a");
+ });
+ });
+
+ it("should exclude a specific room when emitting", done => {
+ const io = new Server(0);
+
+ const nsp = io.of("/nsp");
+
+ const socket1 = createClient(io, "/nsp");
+ const socket2 = createClient(io, "/nsp");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2);
+ }, 300);
+
+ socket1.on("a", () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2);
+ });
+ socket2.on("a", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("should not happen"), socket1, socket2);
+ });
+
+ nsp.on("connection", socket => {
+ socket.on("broadcast", () => {
+ socket.join("room1");
+ nsp.except("room1").emit("a");
+ });
+ });
+
+ socket2.emit("broadcast");
+ });
+
+ it("should emit an 'new_namespace' event", done => {
+ const io = new Server();
+
+ io.on("new_namespace", namespace => {
+ expect(namespace.name).toBe("/nsp");
+ done();
+ });
+
+ io.of("/nsp");
+ });
+
+ it("should not clean up a non-dynamic namespace", done => {
+ const io = new Server(0, { cleanupEmptyChildNamespaces: true });
+ const c1 = createClient(io, "/chat");
+
+ c1.on("connect", () => {
+ c1.disconnect();
+
+ // Give it some time to disconnect the client
+ setTimeout(() => {
+ try {
+ expect(io._nsps.has("/chat")).toBe(true);
+ expect(io._nsps.get("/chat")!.sockets.size).toBe(0);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ }, 100);
+ });
+
+ io.of("/chat");
+ });
+
+ describe("dynamic namespaces", () => {
+ it("should allow connections to dynamic namespaces with a regex", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/dynamic-101");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ const partialDone = createPartialDone(4, () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+
+ let dynamicNsp = io
+ .of(/^\/dynamic-\d+$/)
+ .on("connect", socket => {
+ try {
+ expect(socket.nsp.name).toBe("/dynamic-101");
+ dynamicNsp.emit("hello", 1, "2", { 3: "4" });
+ partialDone();
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ })
+ .use((socket, next) => {
+ next();
+ partialDone();
+ });
+ socket.on("connect_error", err => {
+ clearTimeout(timeout);
+ fail(done, io, err, socket);
+ });
+ socket.on("connect", () => {
+ partialDone();
+ });
+ socket.on("hello", (a, b, c) => {
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe("2");
+ expect(c).toStrictEqual({ 3: "4" });
+ partialDone();
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should allow connections to dynamic namespaces with a function", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/dynamic-101");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.of((name, query, next) => next(null, "/dynamic-101" === name));
+ socket.on("connect", () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+
+ it("should disallow connections when no dynamic namespace matches", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/abc");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.of(/^\/dynamic-\d+$/);
+ io.of((name, query, next) => next(null, "/dynamic-101" === name));
+
+ socket.on("connect_error", err => {
+ clearTimeout(timeout);
+ try {
+ expect(err.message).toBe("Invalid namespace");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should emit an 'new_namespace' event for a dynamic namespace", done => {
+ const io = new Server(0);
+ io.of(/^\/dynamic-\d+$/);
+ const socket = createClient(io, "/dynamic-101");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+ io.on("new_namespace", namespace => {
+ clearTimeout(timeout);
+ try {
+ expect(namespace.name).toBe("/dynamic-101");
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should handle race conditions with dynamic namespaces (#4136)", done => {
+ const io = new Server(0);
+ let timeout: Timer;
+ const counters = {
+ connected: 0,
+ created: 0,
+ events: 0,
+ };
+ const buffer: Function[] = [];
+ io.on("new_namespace", namespace => {
+ counters.created++;
+ });
+
+ const handler = () => {
+ if (++counters.events === 2) {
+ clearTimeout(timeout);
+ try {
+ expect(counters.created).toBe(1);
+ success(done, io, one, two);
+ } catch (err) {
+ fail(done, io, err, one, two);
+ }
+ }
+ };
+
+ io.of((name, query, next) => {
+ buffer.push(next);
+ if (buffer.length === 2) {
+ buffer.forEach(next => next(null, true));
+ }
+ }).on("connection", socket => {
+ if (++counters.connected === 2) {
+ io.of("/dynamic-101").emit("message");
+ }
+ });
+
+ let one = createClient(io, "/dynamic-101");
+ let two = createClient(io, "/dynamic-101");
+ timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), one, two);
+ }, 300);
+ one.on("message", handler);
+ two.on("message", handler);
+ });
+
+ it("should clean up namespace when cleanupEmptyChildNamespaces is on and there are no more sockets in a namespace", done => {
+ const io = new Server(0, { cleanupEmptyChildNamespaces: true });
+ const c1 = createClient(io, "/dynamic-101");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1);
+ }, 300);
+
+ c1.on("connect", () => {
+ c1.disconnect();
+
+ // Give it some time to disconnect and clean up the namespace
+ setTimeout(() => {
+ clearTimeout(timeout);
+ try {
+ expect(io._nsps.has("/dynamic-101")).toBe(false);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err, c1);
+ }
+ }, 100);
+ });
+
+ io.of(/^\/dynamic-\d+$/);
+ });
+
+ it("should allow a client to connect to a cleaned up namespace", done => {
+ const io = new Server(0, { cleanupEmptyChildNamespaces: true });
+ const c1 = createClient(io, "/dynamic-101");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1);
+ }, 300);
+ c1.on("connect", () => {
+ c1.disconnect();
+
+ // Give it some time to disconnect and clean up the namespace
+ setTimeout(() => {
+ try {
+ expect(io._nsps.has("/dynamic-101")).toBe(false);
+
+ const c2 = createClient(io, "/dynamic-101");
+
+ c2.on("connect", () => {
+ clearTimeout(timeout);
+ success(done, io, c2);
+ });
+
+ c2.on("connect_error", () => {
+ clearTimeout(timeout);
+ fail(done, io, new Error("Client got error when connecting to dynamic namespace"), c1);
+ });
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, c1);
+ }
+ }, 100);
+ });
+
+ io.of(/^\/dynamic-\d+$/);
+ });
+
+ it("should not clean up namespace when cleanupEmptyChildNamespaces is off and there are no more sockets in a namespace", done => {
+ const io = new Server(0);
+ const c1 = createClient(io, "/dynamic-101");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), c1);
+ }, 300);
+ c1.on("connect", () => {
+ c1.disconnect();
+
+ // Give it some time to disconnect and clean up the namespace
+ setTimeout(() => {
+ clearTimeout(timeout);
+ try {
+ expect(io._nsps.has("/dynamic-101")).toBe(true);
+ expect(io._nsps.get("/dynamic-101")!.sockets.size).toBe(0);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err, c1);
+ }
+ }, 100);
+ });
+
+ io.of(/^\/dynamic-\d+$/);
+ });
+
+ it("should attach a child namespace to its parent upon manual creation", done => {
+ const io = new Server(0);
+ const parentNamespace = io.of(/^\/dynamic-\d+$/);
+ const childNamespace = io.of("/dynamic-101");
+
+ try {
+ // @ts-ignore
+ expect(parentNamespace.children.has(childNamespace)).toBe(true);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-server-attachment.test.ts b/test/js/third_party/socket.io/socket.io-server-attachment.test.ts
new file mode 100644
index 000000000..15398cdb1
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-server-attachment.test.ts
@@ -0,0 +1,235 @@
+import { Server } from "socket.io";
+import { createServer } from "http";
+import request from "supertest";
+import { getPort, success, fail } from "./support/util";
+import { describe, it, expect } from "bun:test";
+
+describe("server attachment", () => {
+ describe("http.Server", () => {
+ const clientVersion = require("socket.io-client/package.json").version;
+
+ const testSource = filename => done => {
+ const srv = createServer();
+ const io = new Server(srv);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 1000);
+
+ request(srv)
+ .get("/socket.io/" + filename)
+ .buffer(true)
+ .end((err, res) => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err);
+ try {
+ expect(res.headers["content-type"]).toBe("application/javascript; charset=utf-8");
+ expect(res.headers.etag).toBe('"' + clientVersion + '"');
+ expect(res.headers["x-sourcemap"]).toBe(undefined);
+ expect(res.text).toMatch(/engine\.io/);
+ expect(res.status).toBe(200);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ };
+
+ const testSourceMap = filename => done => {
+ const srv = createServer();
+ const io = new Server(srv);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+ request(srv)
+ .get("/socket.io/" + filename)
+ .buffer(true)
+ .end((err, res) => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err);
+ try {
+ expect(res.headers["content-type"]).toBe("application/json; charset=utf-8");
+ expect(res.headers.etag).toBe('"' + clientVersion + '"');
+ expect(res.text).toMatch(/engine\.io/);
+ expect(res.status).toBe(200);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ };
+
+ it("should serve client", testSource("socket.io.js"));
+ it("should serve client with query string", testSource("socket.io.js?buster=" + Date.now()));
+ it("should serve source map", testSourceMap("socket.io.js.map"));
+ it("should serve client (min)", testSource("socket.io.min.js"));
+
+ it("should serve source map (min)", testSourceMap("socket.io.min.js.map"));
+
+ it.skip("should serve client (gzip)", done => {
+ const srv = createServer();
+ const io = new Server(srv);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+ request(srv)
+ .get("/socket.io/socket.io.js")
+ .set("accept-encoding", "gzip,br,deflate")
+ .buffer(true)
+ .end((err, res) => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err);
+ try {
+ expect(res.headers["content-encoding"]).toBe("gzip");
+ expect(res.status).toBe(200);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ it("should serve bundle with msgpack parser", testSource("socket.io.msgpack.min.js"));
+
+ it("should serve source map for bundle with msgpack parser", testSourceMap("socket.io.msgpack.min.js.map"));
+
+ it("should serve the ESM bundle", testSource("socket.io.esm.min.js"));
+
+ it("should serve the source map for the ESM bundle", testSourceMap("socket.io.esm.min.js.map"));
+
+ it("should handle 304", done => {
+ const srv = createServer();
+ const io = new Server(srv);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+ request(srv)
+ .get("/socket.io/socket.io.js")
+ .set("If-None-Match", '"' + clientVersion + '"')
+ .end((err, res) => {
+ try {
+ clearTimeout(timeout);
+ if (err) return done(err);
+ expect(res.statusCode).toBe(304);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ it("should handle 304", done => {
+ const srv = createServer();
+ const io = new Server(srv);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+ request(srv)
+ .get("/socket.io/socket.io.js")
+ .set("If-None-Match", 'W/"' + clientVersion + '"')
+ .end((err, res) => {
+ try {
+ clearTimeout(timeout);
+ if (err) return done(err);
+ expect(res.statusCode).toBe(304);
+ success(done, io);
+ } catch (err) {
+ fail(done, io, err);
+ }
+ });
+ });
+
+ it("should not serve static files", done => {
+ const srv = createServer();
+ const io = new Server(srv, { serveClient: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ request(srv)
+ .get("/socket.io/socket.io.js")
+ .expect(400, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err);
+ success(done, io);
+ });
+ });
+
+ it("should work with #attach", done => {
+ const srv = createServer((req, res) => {
+ res.writeHead(404);
+ res.end();
+ });
+ const sockets = new Server();
+ const timeout = setTimeout(() => {
+ fail(done, sockets, new Error("timeout"));
+ }, 200);
+ sockets.attach(srv);
+ request(srv)
+ .get("/socket.io/socket.io.js")
+ .end((err, res) => {
+ try {
+ clearTimeout(timeout);
+ if (err) return done(err);
+ expect(res.statusCode).toBe(200);
+ success(done, sockets);
+ } catch (err) {
+ fail(done, sockets, err);
+ }
+ });
+ });
+
+ it("should work with #attach (and merge options)", done => {
+ const srv = createServer((req, res) => {
+ res.writeHead(404);
+ res.end();
+ });
+ const server = new Server({
+ pingTimeout: 6000,
+ });
+ try {
+ server.attach(srv, {
+ pingInterval: 24000,
+ });
+ // @ts-ignore
+ expect(server.eio.opts.pingTimeout).toBe(6000);
+ // @ts-ignore
+ expect(server.eio.opts.pingInterval).toBe(24000);
+ success(done, server);
+ } catch (err) {
+ fail(done, server, err);
+ }
+ });
+ });
+
+ describe("port", () => {
+ it("should be bound", done => {
+ const io = new Server(0);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ request(`http://localhost:${getPort(io)}`)
+ .get("/socket.io/socket.io.js")
+ .expect(200, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err);
+ success(done, io);
+ });
+ });
+
+ it("with listen", done => {
+ const io = new Server().listen(0);
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"));
+ }, 200);
+
+ request(`http://localhost:${getPort(io)}`)
+ .get("/socket.io/socket.io.js")
+ .expect(200, err => {
+ clearTimeout(timeout);
+ if (err) return fail(done, io, err);
+ success(done, io);
+ });
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-socket-middleware.test.ts b/test/js/third_party/socket.io/socket.io-socket-middleware.test.ts
new file mode 100644
index 000000000..4eea39491
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-socket-middleware.test.ts
@@ -0,0 +1,89 @@
+// TODO: uncomment when Blob bug in isBinary is fixed
+
+import { Server } from "socket.io";
+import { describe, it, expect } from "bun:test";
+
+import { success, fail, createClient } from "./support/util.ts";
+
+describe("socket middleware", () => {
+ it.skip("should call functions", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ clientSocket.emit("join", "woot");
+
+ let run = 0;
+
+ io.on("connection", socket => {
+ socket.use((event, next) => {
+ try {
+ expect(event).toStrictEqual(["join", "woot"]);
+ event.unshift("wrap");
+ run++;
+ next();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, clientSocket);
+ }
+ });
+ socket.use((event, next) => {
+ try {
+ expect(event).toStrictEqual(["wrap", "join", "woot"]);
+ run++;
+ next();
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, clientSocket);
+ }
+ });
+ socket.on("wrap", (data1, data2) => {
+ try {
+ clearTimeout(timeout);
+ expect(data1).toBe("join");
+ expect(data2).toBe("woot");
+ expect(run).toBe(2);
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+ });
+
+ it.skip("should pass errors", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ clientSocket.emit("join", "woot");
+
+ io.on("connection", socket => {
+ socket.use((event, next) => {
+ next(new Error("Authentication error"));
+ });
+ socket.use((event, next) => {
+ done(new Error("should not happen"));
+ });
+ socket.on("join", () => {
+ done(new Error("should not happen"));
+ });
+ socket.on("error", err => {
+ try {
+ clearTimeout(timeout);
+ expect(err).toBeInstanceOf(Error);
+ expect(err.message).toBe("Authentication error");
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-socket-timeout.test.ts b/test/js/third_party/socket.io/socket.io-socket-timeout.test.ts
new file mode 100644
index 000000000..9bcb4f53b
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-socket-timeout.test.ts
@@ -0,0 +1,137 @@
+// TODO: uncomment when Blob bug in isBinary is fixed
+
+import { Server } from "socket.io";
+import { describe, it, expect } from "bun:test";
+
+import { success, fail, createClient } from "./support/util.ts";
+
+describe("timeout", () => {
+ it.skip("should timeout if the client does not acknowledge the event", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ try {
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 200);
+
+ io.on("connection", socket => {
+ socket.timeout(50).emit("unknown", err => {
+ clearTimeout(timeout);
+ try {
+ expect(err).toBeInstanceOf(Error);
+ success(done, io, client);
+ } catch (err) {
+ fail(done, io, err, client);
+ }
+ });
+ });
+ } catch (err) {
+ fail(done, io, err, client);
+ }
+ });
+
+ it.skip("should timeout if the client does not acknowledge the event in time", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 500);
+
+ client.on("echo", (arg, cb) => {
+ cb(arg);
+ });
+
+ let count = 0;
+
+ io.on("connection", socket => {
+ socket.timeout(0).emit("echo", 42, err => {
+ try {
+ expect(err).toBeInstanceOf(Error);
+ count++;
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, client);
+ }
+ });
+ });
+
+ setTimeout(() => {
+ clearTimeout(timeout);
+ try {
+ expect(count).toBe(1);
+ success(done, io, client);
+ } catch (err) {
+ fail(done, io, err, client);
+ }
+ }, 200);
+ });
+
+ it.skip("should not timeout if the client does acknowledge the event", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 200);
+
+ client.on("echo", (arg, cb) => {
+ cb(arg);
+ });
+
+ io.on("connection", socket => {
+ socket.timeout(50).emit("echo", 42, (err, value) => {
+ clearTimeout(timeout);
+ try {
+ expect(err).toBe(null);
+ expect(value).toBe(42);
+ success(done, io, client);
+ } catch (err) {
+ fail(done, io, err, client);
+ }
+ });
+ });
+ });
+
+ it.skip("should timeout if the client does not acknowledge the event (promise)", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 200);
+
+ io.on("connection", async socket => {
+ try {
+ await socket.timeout(50).emitWithAck("unknown");
+ clearTimeout(timeout);
+ fail(done, io, new Error("timeout"), client);
+ } catch (err) {
+ clearTimeout(timeout);
+ expect(err).toBeInstanceOf(Error);
+ success(done, io, client);
+ }
+ });
+ });
+
+ it.skip("should not timeout if the client does acknowledge the event (promise)", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ const timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 200);
+
+ client.on("echo", (arg, cb) => {
+ cb(arg);
+ });
+
+ io.on("connection", async socket => {
+ try {
+ const value = await socket.timeout(50).emitWithAck("echo", 42);
+ clearTimeout(timeout);
+ expect(value).toBe(42);
+ success(done, io, client);
+ } catch (err) {
+ clearTimeout(timeout);
+ fail(done, io, err, client);
+ }
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-utility-methods.test.ts b/test/js/third_party/socket.io/socket.io-utility-methods.test.ts
new file mode 100644
index 000000000..2326e92c3
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-utility-methods.test.ts
@@ -0,0 +1,181 @@
+import { createServer } from "http";
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { io as ioc, Socket as ClientSocket } from "socket.io-client";
+import { Adapter, BroadcastOptions } from "socket.io-adapter";
+import type { AddressInfo } from "net";
+import { Server } from "socket.io";
+
+import { createPartialDone } from "./support/util.ts";
+
+const SOCKETS_COUNT = 3;
+
+class DummyAdapter extends Adapter {
+ fetchSockets(opts: BroadcastOptions): Promise<any[]> {
+ return Promise.resolve([
+ {
+ id: "42",
+ handshake: {
+ headers: {
+ accept: "*/*",
+ },
+ query: {
+ transport: "polling",
+ EIO: "4",
+ },
+ },
+ rooms: ["42", "room1"],
+ data: {
+ username: "john",
+ },
+ },
+ ]);
+ }
+}
+
+describe("utility methods", () => {
+ let io: Server, clientSockets: ClientSocket[], serverSockets: Socket[];
+ beforeEach(done => {
+ const srv = createServer();
+ io = new Server(srv);
+ const timeout = setTimeout(() => {
+ serverSockets = [];
+ // done(new Error("timeout"));
+ done();
+ }, 300);
+
+ srv.listen(() => {
+ const port = (srv.address() as AddressInfo).port;
+
+ clientSockets = [];
+ for (let i = 0; i < SOCKETS_COUNT; i++) {
+ clientSockets.push(
+ ioc(`http://localhost:${port}`, {
+ // FIXME needed so that clients are properly closed
+ transports: ["websocket"],
+ }),
+ );
+ }
+
+ serverSockets = [];
+ io.on("connection", (socket: Socket) => {
+ serverSockets.push(socket);
+ if (serverSockets.length === SOCKETS_COUNT) {
+ clearTimeout(timeout);
+ done();
+ }
+ });
+ });
+ });
+
+ afterEach(() => {
+ io.close();
+ clientSockets.forEach(socket => socket.disconnect());
+ });
+
+ describe("fetchSockets", () => {
+ it.skip("returns all socket instances", async () => {
+ const sockets = await io.fetchSockets();
+ expect(sockets.length).toBe(3);
+ });
+
+ it.skip("returns all socket instances in the given room", async () => {
+ serverSockets[0]?.join(["room1", "room2"]);
+ serverSockets[1]?.join("room1");
+ serverSockets[2]?.join("room2");
+ const sockets = await io.in("room1").fetchSockets();
+ expect(sockets.length).toBe(2);
+ });
+
+ it.skip("works with a custom adapter", async () => {
+ io.adapter(DummyAdapter);
+ const sockets = await io.fetchSockets();
+ expect(sockets.length).toBe(1);
+ const remoteSocket = sockets[0];
+ expect(remoteSocket.id).toBe("42");
+ expect(remoteSocket.rooms).toStrictEqual(new Set(["42", "room1"]));
+ expect(remoteSocket.data).toStrictEqual({ username: "john" });
+ });
+ });
+
+ describe("socketsJoin", () => {
+ it("makes all socket instances join the given room", () => {
+ io.socketsJoin("room1");
+ serverSockets.forEach(socket => {
+ expect(socket.rooms).toContain("room1");
+ });
+ });
+
+ it.skip("makes all socket instances in a room join the given room", () => {
+ serverSockets[0]?.join(["room1", "room2"]);
+ serverSockets[1]?.join("room1");
+ serverSockets[2]?.join("room2");
+ io.in("room1").socketsJoin("room3");
+ expect(serverSockets[0]?.rooms).toContain("room3");
+ expect(serverSockets[1]?.rooms).toContain("room3");
+ expect(serverSockets[2]?.rooms).not.toContain("room3");
+ });
+ });
+
+ describe("socketsLeave", () => {
+ it.skip("makes all socket instances leave the given room", () => {
+ serverSockets[0]?.join(["room1", "room2"]);
+ serverSockets[1]?.join("room1");
+ serverSockets[2]?.join("room2");
+ io.socketsLeave("room1");
+ expect(serverSockets[0]?.rooms).toContain("room2");
+ expect(serverSockets[0]?.rooms).toContain("room1");
+ expect(serverSockets[1]?.rooms).not.toContain("room1");
+ });
+
+ it.skip("makes all socket instances in a room leave the given room", () => {
+ serverSockets[0]?.join(["room1", "room2"]);
+ serverSockets[1]?.join("room1");
+ serverSockets[2]?.join("room2");
+ io.in("room2").socketsLeave("room1");
+ expect(serverSockets[0]?.rooms).toContain("room2");
+ expect(serverSockets[0]?.rooms).not.toContain("room1");
+ expect(serverSockets[1]?.rooms).toContain("room1");
+ });
+ });
+
+ describe("disconnectSockets", () => {
+ it.skip("makes all socket instances disconnect", done => {
+ io.disconnectSockets(true);
+ const timeout = setTimeout(() => {
+ done(new Error("timeout"));
+ }, 300);
+
+ const partialDone = createPartialDone(3, err => {
+ clearTimeout(timeout);
+ done(err);
+ });
+
+ clientSockets[0].on("disconnect", partialDone);
+ clientSockets[1].on("disconnect", partialDone);
+ clientSockets[2].on("disconnect", partialDone);
+ });
+
+ it.skip("makes all socket instances in a room disconnect", done => {
+ const timeout = setTimeout(() => {
+ done(new Error("timeout"));
+ }, 300);
+
+ serverSockets[0]?.join(["room1", "room2"]);
+ serverSockets[1]?.join("room1");
+ serverSockets[2]?.join("room2");
+ io.in("room2").disconnectSockets(true);
+
+ const partialDone = createPartialDone(2, err => {
+ clearTimeout(timeout);
+ clientSockets[1].off("disconnect");
+ done(err);
+ });
+
+ clientSockets[0].on("disconnect", partialDone);
+ clientSockets[1].on("disconnect", () => {
+ done(new Error("should not happen"));
+ });
+ clientSockets[2].on("disconnect", partialDone);
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io-uws.test.ts b/test/js/third_party/socket.io/socket.io-uws.test.ts
new file mode 100644
index 000000000..4749caa7a
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io-uws.test.ts
@@ -0,0 +1,208 @@
+// import { App, us_socket_local_port, us_listen_socket_close } from "uWebSockets.js";
+// uWS throws an error when trying to import it
+import { Server } from "socket.io";
+import { io as ioc, Socket as ClientSocket } from "socket.io-client";
+import request from "supertest";
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+
+const createPartialDone = (done: (err?: Error) => void, count: number) => {
+ let i = 0;
+ return () => {
+ if (++i === count) {
+ done();
+ } else if (i > count) {
+ done(new Error(`partialDone() called too many times: ${i} > ${count}`));
+ }
+ };
+};
+
+const shouldNotHappen = done => () => done(new Error("should not happen"));
+
+describe("socket.io with uWebSocket.js-based engine", () => {
+ let io: Server,
+ uwsSocket: any,
+ port: number,
+ client: ClientSocket,
+ clientWSOnly: ClientSocket,
+ clientPollingOnly: ClientSocket,
+ clientCustomNamespace: ClientSocket;
+
+ beforeEach(done => {
+ const app = App();
+ io = new Server();
+ io.attachApp(app);
+
+ io.of("/custom");
+
+ app.listen(0, listenSocket => {
+ uwsSocket = listenSocket;
+ port = us_socket_local_port(listenSocket);
+
+ client = ioc(`http://localhost:${port}`);
+ clientWSOnly = ioc(`http://localhost:${port}`, {
+ transports: ["websocket"],
+ });
+ clientPollingOnly = ioc(`http://localhost:${port}`, {
+ transports: ["polling"],
+ });
+ clientCustomNamespace = ioc(`http://localhost:${port}/custom`);
+ });
+
+ const partialDone = createPartialDone(done, 4);
+ client.on("connect", partialDone);
+ clientWSOnly.once("connect", partialDone);
+ clientPollingOnly.on("connect", partialDone);
+ clientCustomNamespace.on("connect", partialDone);
+ });
+
+ afterEach(() => {
+ io.close();
+ us_listen_socket_close(uwsSocket);
+
+ client.disconnect();
+ clientWSOnly.disconnect();
+ clientPollingOnly.disconnect();
+ clientCustomNamespace.disconnect();
+ });
+
+ it.skip("should broadcast", done => {
+ const partialDone = createPartialDone(done, 3);
+
+ client.on("hello", partialDone);
+ clientWSOnly.on("hello", partialDone);
+ clientPollingOnly.on("hello", partialDone);
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ io.emit("hello");
+ });
+
+ it.skip("should broadcast in a namespace", done => {
+ client.on("hello", shouldNotHappen(done));
+ clientWSOnly.on("hello", shouldNotHappen(done));
+ clientPollingOnly.on("hello", shouldNotHappen(done));
+ clientCustomNamespace.on("hello", done);
+
+ io.of("/custom").emit("hello");
+ });
+
+ it.skip("should broadcast in a dynamic namespace", done => {
+ const dynamicNamespace = io.of(/\/dynamic-\d+/);
+ const dynamicClient = clientWSOnly.io.socket("/dynamic-101");
+
+ dynamicClient.on("connect", () => {
+ dynamicNamespace.emit("hello");
+ });
+
+ dynamicClient.on("hello", () => {
+ dynamicClient.disconnect();
+ done();
+ });
+ });
+
+ it.skip("should broadcast binary content", done => {
+ const partialDone = createPartialDone(done, 3);
+
+ client.on("hello", partialDone);
+ clientWSOnly.on("hello", partialDone);
+ clientPollingOnly.on("hello", partialDone);
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ io.emit("hello", Buffer.from([1, 2, 3]));
+ });
+
+ it.skip("should broadcast volatile packet with binary content", done => {
+ const partialDone = createPartialDone(done, 3);
+
+ client.on("hello", partialDone);
+ clientWSOnly.on("hello", partialDone);
+ clientPollingOnly.on("hello", partialDone);
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ // wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ io.volatile.emit("hello", Buffer.from([1, 2, 3]));
+ }, 20);
+ });
+
+ it.skip("should broadcast in a room", done => {
+ const partialDone = createPartialDone(done, 2);
+
+ client.on("hello", shouldNotHappen(done));
+ clientWSOnly.on("hello", partialDone);
+ clientPollingOnly.on("hello", partialDone);
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
+ io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
+
+ io.to("room1").emit("hello");
+ });
+
+ it.skip("should broadcast in multiple rooms", done => {
+ const partialDone = createPartialDone(done, 2);
+
+ client.on("hello", shouldNotHappen(done));
+ clientWSOnly.on("hello", partialDone);
+ clientPollingOnly.on("hello", partialDone);
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
+ io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
+
+ io.to(["room1", "room2"]).emit("hello");
+ });
+
+ it.skip("should broadcast in all but a given room", done => {
+ const partialDone = createPartialDone(done, 2);
+
+ client.on("hello", partialDone);
+ clientWSOnly.on("hello", partialDone);
+ clientPollingOnly.on("hello", shouldNotHappen(done));
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
+ io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
+
+ io.except("room2").emit("hello");
+ });
+
+ it.skip("should work even after leaving room", done => {
+ const partialDone = createPartialDone(done, 2);
+
+ client.on("hello", partialDone);
+ clientWSOnly.on("hello", shouldNotHappen(done));
+ clientPollingOnly.on("hello", partialDone);
+ clientCustomNamespace.on("hello", shouldNotHappen(done));
+
+ io.of("/").sockets.get(client.id)!.join("room1");
+ io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
+
+ io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
+ io.of("/").sockets.get(clientWSOnly.id)!.leave("room1");
+
+ io.to("room1").emit("hello");
+ });
+
+ it.skip("should not crash when socket is disconnected before the upgrade", done => {
+ client.on("disconnect", () => done());
+
+ io.of("/").sockets.get(client.id)!.disconnect();
+ });
+
+ it.skip("should serve static files", done => {
+ const clientVersion = require("socket.io-client/package.json").version;
+
+ request(`http://localhost:${port}`)
+ .get("/socket.io/socket.io.js")
+ .buffer(true)
+ .end((err, res) => {
+ if (err) return done(err);
+ expect(res.headers["content-type"]).toBe("application/javascript; charset=utf-8");
+ expect(res.headers.etag).toBe('"' + clientVersion + '"');
+ expect(res.headers["x-sourcemap"]).toBe(undefined);
+ expect(res.text).toMatch(/engine\.io/);
+ expect(res.status).toBe(200);
+ done();
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/socket.io.test.ts b/test/js/third_party/socket.io/socket.io.test.ts
new file mode 100644
index 000000000..9d527f6b7
--- /dev/null
+++ b/test/js/third_party/socket.io/socket.io.test.ts
@@ -0,0 +1,1554 @@
+import { describe, it, expect } from "bun:test";
+
+import fs from "fs";
+import { join } from "path";
+import { createClient, createPartialDone, getPort, success, fail } from "./support/util.ts";
+import { Server } from "socket.io";
+
+describe("socket", () => {
+ it("should not fire events more than once after manually reconnecting", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+ clientSocket.on("connect", function init() {
+ clientSocket.off("connect", init);
+ clientSocket.io.engine.close();
+
+ process.nextTick(() => {
+ clientSocket.connect();
+ });
+ clientSocket.on("connect", () => {
+ clearTimeout(timeout);
+ success(done, io, clientSocket);
+ });
+ });
+ });
+
+ it.skip("should not fire reconnect_failed event more than once when server closed", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", {
+ reconnectionAttempts: 3,
+ reconnectionDelay: 100,
+ });
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 200);
+
+ clientSocket.on("connect", () => {
+ io.close();
+ });
+
+ clientSocket.io.on("reconnect_failed", () => {
+ clearTimeout(timeout);
+ success(done, io, clientSocket);
+ });
+ });
+
+ it("should receive events", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("random", (a, b, c) => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe("2");
+ expect(c).toStrictEqual([3]);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ socket.emit("random", 1, "2", [3]);
+ });
+ });
+
+ it("should receive message events through `send`", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("message", a => {
+ clearInterval(timeout);
+ try {
+ expect(a).toBe(1337);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ socket.send(1337);
+ });
+ });
+
+ it("should error with null messages", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ io.on("connection", s => {
+ s.on("message", a => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe(null);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ socket.send(null);
+ });
+ });
+
+ it("should handle transport null messages", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ io.on("connection", s => {
+ s.on("error", err => {
+ try {
+ expect(err).toBeInstanceOf(Error);
+ } catch (err) {
+ fail(done, io, err, socket);
+ return;
+ }
+ s.on("disconnect", reason => {
+ clearTimeout(timeout);
+ try {
+ expect(reason).toBe("forced close");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+ (s as any).client.ondata(null);
+ });
+ });
+
+ it("should emit events", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ socket.on("woot", a => {
+ clearTimeout(timeout);
+
+ try {
+ expect(a).toBe("tobi");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ s.emit("woot", "tobi");
+ });
+ });
+
+ it("should emit events with utf8 multibyte character", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let i = 0;
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ socket.on("hoot", a => {
+ try {
+ expect(a).toBe("utf8 — string");
+ i++;
+
+ if (3 == i) {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ }
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ s.emit("hoot", "utf8 — string");
+ s.emit("hoot", "utf8 — string");
+ s.emit("hoot", "utf8 — string");
+ });
+ });
+
+ it("should emit events with binary data", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ let imageData: any;
+ socket.on("bun", a => {
+ clearTimeout(timeout);
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+ expect(imageData.length).toStrictEqual(a.length);
+ expect(imageData[0]).toStrictEqual(a[0]);
+ expect(imageData[imageData.length - 1]).toStrictEqual(a[a.length - 1]);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ fs.readFile(join(__dirname, "support", "bun.png"), (err, data) => {
+ if (err) return done(err);
+ imageData = data;
+ s.emit("bun", data);
+ });
+ });
+ });
+
+ it.skip("should emit events with several types of data (including binary)", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ socket.on("multiple", (a, b, c, d, e, f) => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe(1);
+ expect(Buffer.isBuffer(b)).toBe(true);
+ expect(c).toBe("3");
+ expect(d).toStrictEqual([4]);
+ expect(Buffer.isBuffer(e)).toBe(true);
+ expect(Buffer.isBuffer(f[0])).toBe(true);
+ expect(f[1]).toBe("swag");
+ expect(Buffer.isBuffer(f[2])).toBe(true);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ fs.readFile(join(__dirname, "support", "bun.png"), (err, data) => {
+ if (err) return done(err);
+ const buf = Buffer.from("asdfasdf", "utf8");
+ s.emit("multiple", 1, data, "3", [4], buf, [data, "swag", buf]);
+ });
+ });
+ });
+
+ it("should receive events with binary data", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("buff", a => {
+ clearTimeout(timeout);
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ const buf = Buffer.from("abcdefg", "utf8");
+ socket.emit("buff", buf);
+ });
+ });
+
+ it("should receive events with several types of data (including binary)", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("multiple", (a, b, c, d, e, f) => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe(1);
+ expect(Buffer.isBuffer(b)).toBe(true);
+ expect(c).toBe("3");
+ expect(d).toStrictEqual([4]);
+ expect(Buffer.isBuffer(e)).toBe(true);
+ expect(Buffer.isBuffer(f[0])).toBe(true);
+ expect(f[1]).toBe("swag");
+ expect(Buffer.isBuffer(f[2])).toBe(true);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ fs.readFile(join(__dirname, "support", "bun.png"), (err, data) => {
+ if (err) return done(err);
+ const buf = Buffer.from("asdfasdf", "utf8");
+ socket.emit("multiple", 1, data, "3", [4], buf, [data, "swag", buf]);
+ });
+ });
+ });
+
+ it("should not emit volatile event after regular event (polling)", done => {
+ const io = new Server(0, { transports: ["polling"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ s.emit("ev", "data");
+ s.volatile.emit("ev", "data");
+ });
+
+ const socket = createClient(io, "/", { transports: ["polling"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it.skip("should not emit volatile event after regular event (ws)", done => {
+ const io = new Server(0, { transports: ["websocket"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ s.emit("ev", "data");
+ s.volatile.emit("ev", "data");
+ });
+
+ const socket = createClient(io, "/", { transports: ["websocket"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it("should emit volatile event (polling)", done => {
+ const io = new Server(0, { transports: ["polling"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.volatile.emit("ev", "data");
+ }, 100);
+ });
+
+ const socket = createClient(io, "/", { transports: ["polling"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 500);
+ });
+
+ it.skip("should emit volatile event (ws)", done => {
+ const io = new Server(0, { transports: ["websocket"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.volatile.emit("ev", "data");
+ }, 20);
+ });
+
+ const socket = createClient(io, "/", { transports: ["websocket"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it("should emit only one consecutive volatile event (polling)", done => {
+ const io = new Server(0, { transports: ["polling"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.volatile.emit("ev", "data");
+ s.volatile.emit("ev", "data");
+ }, 100);
+ });
+
+ const socket = createClient(io, "/", { transports: ["polling"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 500);
+ });
+
+ it.skip("should emit only one consecutive volatile event (ws)", done => {
+ const io = new Server(0, { transports: ["websocket"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.volatile.emit("ev", "data");
+ s.volatile.emit("ev", "data");
+ }, 20);
+ });
+
+ const socket = createClient(io, "/", { transports: ["websocket"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it.skip("should emit only one consecutive volatile event with binary (ws)", done => {
+ const io = new Server(0, { transports: ["websocket"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.volatile.emit("ev", Buffer.from([1, 2, 3]));
+ s.volatile.emit("ev", Buffer.from([4, 5, 6]));
+ }, 20);
+ });
+
+ const socket = createClient(io, "/", { transports: ["websocket"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it.skip("should broadcast only one consecutive volatile event with binary (ws)", done => {
+ const io = new Server(0, { transports: ["websocket"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ io.volatile.emit("ev", Buffer.from([1, 2, 3]));
+ io.volatile.emit("ev", Buffer.from([4, 5, 6]));
+ }, 20);
+ });
+
+ const socket = createClient(io, "/", { transports: ["websocket"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(1);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 2000);
+ });
+
+ it("should emit regular events after trying a failed volatile event (polling)", done => {
+ const io = new Server(0, { transports: ["polling"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.emit("ev", "data");
+ s.volatile.emit("ev", "data");
+ s.emit("ev", "data");
+ }, 20);
+ });
+
+ const socket = createClient(io, "/", { transports: ["polling"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(2);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it.skip("should emit regular events after trying a failed volatile event (ws)", done => {
+ const io = new Server(0, { transports: ["websocket"] });
+
+ let counter = 0;
+ io.on("connection", s => {
+ // Wait to make sure there are no packets being sent for opening the connection
+ setTimeout(() => {
+ s.emit("ev", "data");
+ s.volatile.emit("ev", "data");
+ s.emit("ev", "data");
+ }, 20);
+ });
+
+ const socket = createClient(io, "/", { transports: ["websocket"] });
+ socket.on("ev", () => {
+ counter++;
+ });
+
+ setTimeout(() => {
+ try {
+ expect(counter).toBe(2);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ }, 200);
+ });
+
+ it("should emit message events through `send`", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ socket.on("message", a => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe("a");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ s.send("a");
+ });
+ });
+
+ it("should receive event with callbacks", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+ io.on("connection", s => {
+ s.on("woot", fn => {
+ fn(1, 2);
+ });
+ socket.emit("woot", (a, b) => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe(2);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+ });
+
+ it("should receive all events emitted from namespaced client immediately and in order", done => {
+ const io = new Server(0);
+ let total = 0;
+ let timeout: any;
+ io.of("/chat", s => {
+ s.on("hi", letter => {
+ total++;
+ if (total == 2) {
+ clearInterval(timeout);
+ expect(letter).toBe("b");
+ success(done, io, chat);
+ } else if (total == 1 && letter != "a") {
+ fail(done, io, new Error("events out of order"), chat);
+ }
+ });
+ });
+
+ const chat = createClient(io, "/chat");
+ timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), chat);
+ }, 2000);
+
+ chat.emit("hi", "a");
+ setTimeout(() => {
+ chat.emit("hi", "b");
+ }, 50);
+ });
+
+ it("should emit events with callbacks", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ socket.on("hi", fn => {
+ fn();
+ });
+ s.emit("hi", () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+ });
+
+ it("should receive events with args and callback", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("woot", (a, b, fn) => {
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe(2);
+ fn();
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ socket.emit("woot", 1, 2, () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+ });
+
+ it("should emit events with args and callback", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ socket.on("hi", (a, b, fn) => {
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe(2);
+ fn();
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ s.emit("hi", 1, 2, () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+ });
+
+ it("should receive events with binary args and callbacks", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("woot", (buf, fn) => {
+ try {
+ expect(Buffer.isBuffer(buf)).toBe(true);
+ fn(1, 2);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ socket.emit("woot", Buffer.alloc(3), (a, b) => {
+ clearTimeout(timeout);
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe(2);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+ });
+
+ it("should emit events with binary args and callback", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ socket.on("hi", (a, fn) => {
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+ fn();
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ s.emit("hi", Buffer.alloc(4), () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+ });
+
+ it("should emit events and receive binary data in a callback", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ socket.on("hi", fn => {
+ fn(Buffer.alloc(1));
+ });
+ s.emit("hi", a => {
+ clearTimeout(timeout);
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+ });
+
+ it("should receive events and pass binary data in a callback", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ s.on("woot", fn => {
+ fn(Buffer.alloc(2));
+ });
+ socket.emit("woot", a => {
+ clearTimeout(timeout);
+ try {
+ expect(Buffer.isBuffer(a)).toBe(true);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+ });
+
+ it("should emit an event and wait for the acknowledgement", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", async s => {
+ socket.on("hi", (a, b, fn) => {
+ try {
+ expect(a).toBe(1);
+ expect(b).toBe(2);
+ fn(3);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+
+ const val = await s.emitWithAck("hi", 1, 2);
+ clearTimeout(timeout);
+ try {
+ expect(val).toBe(3);
+
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should have access to the client", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ expect(typeof s.client).toBe("object");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should have access to the connection", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ expect(typeof s.client.conn).toBe("object");
+ expect(typeof s.conn).toBe("object");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should have access to the request", done => {
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ expect(typeof s.client.request.headers).toBe("object");
+ expect(typeof s.request.headers).toBe("object");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should see query parameters in the request", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/", { query: { key1: 1, key2: 2 } });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 200);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ const parsed = require("url").parse(s.request.url);
+ const query = require("querystring").parse(parsed.query);
+ expect(query.key1).toBe("1");
+ expect(query.key2).toBe("2");
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ it("should see query parameters sent from secondary namespace connections in handshake object", done => {
+ const io = new Server(0);
+ const client1 = createClient(io);
+ const client2 = createClient(io, "/connection2", {
+ auth: { key1: "aa", key2: "&=bb" },
+ });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client1, client2);
+ }, 200);
+
+ io.on("connection", s => {});
+ io.of("/connection2").on("connection", s => {
+ clearTimeout(timeout);
+
+ try {
+ expect(s.handshake.query.key1).toBe(undefined);
+ expect(s.handshake.query.EIO).toBe("4");
+ expect(s.handshake.auth.key1).toBe("aa");
+ expect(s.handshake.auth.key2).toBe("&=bb");
+ success(done, io, client1, client2);
+ } catch (err) {
+ fail(done, io, err, client1, client2);
+ }
+ });
+ });
+
+ it.skip("should handle very large json", function (done) {
+ const io = new Server(0, { perMessageDeflate: false });
+ let received = 0;
+
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 30000);
+
+ socket.on("big", a => {
+ try {
+ expect(Buffer.isBuffer(a.json)).toBe(false);
+ if (++received == 3) {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ } else socket.emit("big", a);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ fs.readFile(join(__dirname, "fixtures", "big.json"), (err, data: any) => {
+ if (err) return done(err);
+ data = JSON.parse(data);
+ s.emit("big", { hello: "friend", json: data });
+ });
+ s.on("big", a => {
+ s.emit("big", a);
+ });
+ });
+ });
+
+ it.skip("should handle very large binary data", function (done) {
+ const io = new Server(0, { perMessageDeflate: false });
+ let received = 0;
+
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 30000);
+
+ socket.on("big", a => {
+ try {
+ expect(Buffer.isBuffer(a.image)).toBe(true);
+ if (++received == 3) {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ } else socket.emit("big", a);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ io.on("connection", s => {
+ fs.readFile(join(__dirname, "fixtures", "big.jpg"), (err, data) => {
+ if (err) return done(err);
+ s.emit("big", { hello: "friend", image: data });
+ });
+ s.on("big", a => {
+ expect(Buffer.isBuffer(a.image)).toBe(true);
+ s.emit("big", a);
+ });
+ });
+ });
+
+ it("should be able to emit after server close and restart", done => {
+ const io = new Server(0);
+ let timeout: any;
+ io.on("connection", socket => {
+ socket.on("ev", data => {
+ clearTimeout(timeout);
+ try {
+ expect(data).toBe("payload");
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+
+ const port = getPort(io);
+ const clientSocket = createClient(io, "/", {
+ reconnectionAttempts: 10,
+ reconnectionDelay: 100,
+ });
+ timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+ clientSocket.once("connect", () => {
+ io.close(() => {
+ clientSocket.io.on("reconnect", () => {
+ clientSocket.emit("ev", "payload");
+ });
+ io.listen(port);
+ });
+ });
+ });
+
+ it("should enable compression by default", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/chat");
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+ io.of("/chat").on("connection", s => {
+ s.conn.once("packetCreate", packet => {
+ clearTimeout(timeout);
+ try {
+ expect(packet.options.compress).toBe(true);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ s.emit("woot", "hi");
+ });
+ });
+
+ it("should disable compression", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/chat");
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.of("/chat").on("connection", s => {
+ s.conn.once("packetCreate", packet => {
+ clearTimeout(timeout);
+ try {
+ expect(packet.options.compress).toBe(false);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ s.compress(false).emit("woot", "hi");
+ });
+ });
+
+ it.skip("should error with raw binary and warn", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", s => {
+ s.conn.on("upgrade", () => {
+ console.log("\u001b[96mNote: warning expected and normal in test.\u001b[39m");
+ // @ts-ignore
+ socket.io.engine.write("5woooot");
+ setTimeout(() => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ }, 100);
+ });
+ });
+ });
+
+ // TODO: investigate IOT
+ it.skip("should not crash when receiving an error packet without handler", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", s => {
+ s.conn.on("upgrade", () => {
+ console.log("\u001b[96mNote: warning expected and normal in test.\u001b[39m");
+ // @ts-ignore
+ socket.io.engine.write('44["handle me please"]');
+ setTimeout(() => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ }, 100);
+ });
+ });
+ });
+
+ it.skip("should not crash with raw binary", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", s => {
+ s.once("error", err => {
+ clearTimeout(timeout);
+ try {
+ expect(err.message).toMatch(/Illegal attachments/);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ s.conn.on("upgrade", () => {
+ // @ts-ignore
+ socket.io.engine.write("5woooot");
+ });
+ });
+ });
+
+ it.skip("should handle empty binary packet", done => {
+ const io = new Server(0);
+ const socket = createClient(io, "/", { reconnection: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", s => {
+ s.once("error", err => {
+ clearTimeout(timeout);
+ try {
+ expect(err.message).toMatch(/Illegal attachments/);
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ s.conn.on("upgrade", () => {
+ // @ts-ignore
+ socket.io.engine.write("5");
+ });
+ });
+ });
+
+ it("should not crash when messing with Object prototype (and other globals)", done => {
+ // @ts-ignore
+ Object.prototype.foo = "bar";
+ // @ts-ignore
+ global.File = "";
+ // @ts-ignore
+ global.Blob = [];
+ const io = new Server(0);
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", () => {
+ clearTimeout(timeout);
+ success(done, io, socket);
+ });
+ });
+
+ it("should throw on reserved event", done => {
+ const io = new Server(0);
+
+ const socket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket);
+ }, 300);
+
+ io.on("connection", s => {
+ clearTimeout(timeout);
+ try {
+ expect(() => s.emit("connect_error")).toThrow(/"connect_error" is a reserved event name/);
+ socket.close();
+ success(done, io, socket);
+ } catch (err) {
+ fail(done, io, err, socket);
+ }
+ });
+ });
+
+ // TODO: investigate weird error here
+ it.skip("should ignore a packet received after disconnection", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io);
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ io.on("connection", socket => {
+ socket.on("test", () => {
+ fail(done, io, new Error("should not happen"), clientSocket);
+ });
+ socket.on("disconnect", () => {
+ clearTimeout(timeout);
+ success(done, io, clientSocket);
+ });
+ });
+
+ clientSocket.on("connect", () => {
+ clientSocket.emit("test", Buffer.alloc(10));
+ clientSocket.disconnect();
+ });
+ });
+
+ it("should leave all rooms joined after a middleware failure", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 300);
+
+ io.use((socket, next) => {
+ socket.join("room1");
+ next(new Error("nope"));
+ });
+
+ client.on("connect_error", () => {
+ clearTimeout(timeout);
+ try {
+ expect(io.of("/").adapter.rooms.size).toStrictEqual(0);
+
+ io.close();
+ success(done, io, client);
+ } catch (err) {
+ fail(done, io, err, client);
+ }
+ });
+ });
+
+ it("should not join rooms after disconnection", done => {
+ const io = new Server(0);
+ const client = createClient(io, "/");
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), client);
+ }, 300);
+
+ io.on("connection", socket => {
+ socket.disconnect();
+ socket.join("room1");
+ });
+
+ client.on("disconnect", () => {
+ clearTimeout(timeout);
+ try {
+ expect(io.of("/").adapter.rooms.size).toStrictEqual(0);
+
+ io.close();
+ success(done, io, client);
+ } catch (err) {
+ fail(done, io, err, client);
+ }
+ });
+ });
+
+ describe("onAny", () => {
+ it("should call listener", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ clientSocket.emit("my-event", "123");
+
+ io.on("connection", socket => {
+ socket.onAny((event, arg1) => {
+ clearTimeout(timeout);
+ try {
+ expect(event).toBe("my-event");
+ expect(arg1).toBe("123");
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+ });
+
+ it("should prepend listener", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ clientSocket.emit("my-event", "123");
+
+ io.on("connection", socket => {
+ let count = 0;
+
+ socket.onAny((event, arg1) => {
+ clearTimeout(timeout);
+ try {
+ expect(count).toBe(2);
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ socket.prependAny(() => {
+ try {
+ expect(count++).toBe(1);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ socket.prependAny(() => {
+ try {
+ expect(count++).toBe(0);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+ });
+ });
+
+ it("should remove listener", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ clientSocket.emit("my-event", "123");
+
+ io.on("connection", socket => {
+ const _fail = () => fail(done, io, new Error("should not happen"), clientSocket);
+
+ socket.onAny(_fail);
+ socket.offAny(_fail);
+ try {
+ expect(socket.listenersAny.length).toBe(0);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ socket.onAny(() => {
+ clearTimeout(timeout);
+
+ success(done, io, clientSocket);
+ });
+ });
+ });
+ });
+
+ describe("onAnyOutgoing", () => {
+ it("should call listener", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ io.on("connection", socket => {
+ socket.onAnyOutgoing((event, arg1) => {
+ clearTimeout(timeout);
+
+ try {
+ expect(event).toBe("my-event");
+ expect(arg1).toBe("123");
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ socket.emit("my-event", "123");
+ });
+ });
+
+ it("should call listener when broadcasting", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+ io.on("connection", socket => {
+ socket.onAnyOutgoing((event, arg1) => {
+ clearTimeout(timeout);
+ try {
+ expect(event).toBe("my-event");
+ expect(arg1).toBe("123");
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ io.emit("my-event", "123");
+ });
+ });
+
+ it("should call listener when broadcasting binary data", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+ io.on("connection", socket => {
+ socket.onAnyOutgoing((event, arg1) => {
+ clearTimeout(timeout);
+ try {
+ expect(event).toBe("my-event");
+ expect(arg1).toBeInstanceOf(Uint8Array);
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ io.emit("my-event", Uint8Array.of(1, 2, 3));
+ });
+ });
+
+ it("should prepend listener", done => {
+ const io = new Server(0);
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ io.on("connection", socket => {
+ let count = 0;
+
+ socket.onAnyOutgoing((event, arg1) => {
+ clearTimeout(timeout);
+ try {
+ expect(count).toBe(2);
+
+ success(done, io, clientSocket);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ socket.prependAnyOutgoing(() => {
+ try {
+ expect(count++).toBe(1);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ socket.prependAnyOutgoing(() => {
+ try {
+ expect(count++).toBe(0);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+ });
+
+ socket.emit("my-event", "123");
+ });
+ });
+
+ it("should remove listener", done => {
+ const io = new Server(0);
+
+ const clientSocket = createClient(io, "/", { multiplex: false });
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), clientSocket);
+ }, 300);
+
+ io.on("connection", socket => {
+ const _fail = () => fail(done, io, new Error("fail"), clientSocket);
+
+ socket.onAnyOutgoing(_fail);
+ socket.offAnyOutgoing(_fail);
+ try {
+ expect(socket.listenersAnyOutgoing.length).toBe(0);
+ } catch (err) {
+ fail(done, io, err, clientSocket);
+ }
+
+ socket.onAnyOutgoing(() => {
+ clearTimeout(timeout);
+ success(done, io, clientSocket);
+ });
+
+ socket.emit("my-event", "123");
+ });
+ });
+
+ it.skip("should disconnect all namespaces when calling disconnect(true)", done => {
+ const io = new Server(0);
+ io.of("/foo");
+ io.of("/bar");
+
+ const socket1 = createClient(io, "/", {
+ transports: ["websocket"],
+ });
+ const socket2 = createClient(io, "/foo");
+ const socket3 = createClient(io, "/bar");
+
+ let timeout = setTimeout(() => {
+ fail(done, io, new Error("timeout"), socket1, socket2, socket3);
+ }, 300);
+
+ io.of("/bar").on("connection", socket => {
+ socket.disconnect(true);
+ });
+
+ const partialDone = createPartialDone(3, () => {
+ clearTimeout(timeout);
+ success(done, io, socket1, socket2, socket3);
+ });
+
+ socket1.on("disconnect", partialDone);
+ socket2.on("disconnect", partialDone);
+ socket3.on("disconnect", partialDone);
+ });
+ });
+});
diff --git a/test/js/third_party/socket.io/support/bun.png b/test/js/third_party/socket.io/support/bun.png
new file mode 100644
index 000000000..a6469e481
--- /dev/null
+++ b/test/js/third_party/socket.io/support/bun.png
Binary files differ
diff --git a/test/js/third_party/socket.io/support/util.ts b/test/js/third_party/socket.io/support/util.ts
new file mode 100644
index 000000000..597b40d65
--- /dev/null
+++ b/test/js/third_party/socket.io/support/util.ts
@@ -0,0 +1,86 @@
+import type { Server } from "socket.io";
+import request from "supertest";
+
+import { io as ioc, ManagerOptions, Socket as ClientSocket, SocketOptions } from "socket.io-client";
+
+export function createClient(
+ io: Server,
+ nsp: string = "/",
+ opts?: Partial<ManagerOptions & SocketOptions>,
+): ClientSocket {
+ // @ts-ignore
+ const port = io.httpServer.address().port;
+ return ioc(`http://localhost:${port}${nsp}`, opts);
+}
+
+export function success(done: Function, io: Server, ...clients: ClientSocket[]) {
+ io.close();
+ clients.forEach(client => client.disconnect());
+ done();
+}
+
+export function fail(done: Function, io: Server, err: any | unknown, ...clients: ClientSocket[]) {
+ io.close();
+ clients.forEach(client => client.disconnect());
+ done(err);
+}
+
+export function getPort(io: Server): number {
+ // @ts-ignore
+ return io.httpServer.address().port;
+}
+
+export function createPartialDone(count: number, done: (err?: Error) => void) {
+ let i = 0;
+ return () => {
+ if (++i === count) {
+ done();
+ } else if (i > count) {
+ done(new Error(`partialDone() called too many times: ${i} > ${count}`));
+ }
+ };
+}
+
+// TODO: update superagent as latest release now supports promises
+export function eioHandshake(httpServer): Promise<string> {
+ return new Promise(resolve => {
+ request(httpServer)
+ .get("/socket.io/")
+ .query({ transport: "polling", EIO: 4 })
+ .end((err, res) => {
+ const sid = JSON.parse(res.text.substring(1)).sid;
+ resolve(sid);
+ });
+ });
+}
+
+export function eioPush(httpServer, sid: string, body: string): Promise<void> {
+ return new Promise(resolve => {
+ request(httpServer)
+ .post("/socket.io/")
+ .send(body)
+ .query({ transport: "polling", EIO: 4, sid })
+ .expect(200)
+ .end(() => {
+ resolve();
+ });
+ });
+}
+
+export function eioPoll(httpServer, sid): Promise<string> {
+ return new Promise(resolve => {
+ request(httpServer)
+ .get("/socket.io/")
+ .query({ transport: "polling", EIO: 4, sid })
+ .expect(200)
+ .end((err, res) => {
+ resolve(res.text);
+ });
+ });
+}
+
+export function waitFor<T = unknown>(emitter, event) {
+ return new Promise<T>(resolve => {
+ emitter.once(event, resolve);
+ });
+}
diff --git a/test/package.json b/test/package.json
index c2caf7653..49ae422c3 100644
--- a/test/package.json
+++ b/test/package.json
@@ -16,7 +16,10 @@
"lodash": "^4.17.21",
"svelte": "^3.55.1",
"typescript": "^5.0.2",
- "undici": "^5.20.0"
+ "undici": "^5.20.0",
+ "socket.io": "^4.6.1",
+ "socket.io-client": "^4.6.1",
+ "supertest": "^6.1.6"
},
"private": true,
"scripts": {