From 468c3c8008ddd0c89b2fc2054d874e9e706f0948 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Fri, 12 Mar 2021 23:38:05 +0100 Subject: [PATCH] feat: implement utility methods from Socket.IO v4 Reference: https://socket.io/docs/v3/migrating-from-3-x-to-4-0/#Additional-utility-methods --- lib/index.ts | 155 ++++++++++++++++++++++++++++++++++++++++++++- package-lock.json | 158 ++++++++++++++++++++++++++-------------------- package.json | 6 +- test/index.ts | 107 +++++++++++++++++++++++++++++++ test/util.ts | 22 +++++++ 5 files changed, 375 insertions(+), 73 deletions(-) create mode 100644 test/util.ts diff --git a/lib/index.ts b/lib/index.ts index 77354493..9c01f737 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -17,6 +17,7 @@ enum RequestType { REMOTE_JOIN = 2, REMOTE_LEAVE = 3, REMOTE_DISCONNECT = 4, + REMOTE_FETCH = 5, } interface Request { @@ -222,6 +223,14 @@ export class RedisAdapter extends Adapter { break; case RequestType.REMOTE_JOIN: + if (request.opts) { + const opts = { + rooms: new Set(request.opts.rooms), + except: new Set(request.opts.except), + }; + return super.addSockets(opts, request.rooms); + } + socket = this.nsp.sockets.get(request.sid); if (!socket) { return; @@ -237,6 +246,14 @@ export class RedisAdapter extends Adapter { break; case RequestType.REMOTE_LEAVE: + if (request.opts) { + const opts = { + rooms: new Set(request.opts.rooms), + except: new Set(request.opts.except), + }; + return super.delSockets(opts, request.rooms); + } + socket = this.nsp.sockets.get(request.sid); if (!socket) { return; @@ -252,6 +269,14 @@ export class RedisAdapter extends Adapter { break; case RequestType.REMOTE_DISCONNECT: + if (request.opts) { + const opts = { + rooms: new Set(request.opts.rooms), + except: new Set(request.opts.except), + }; + return super.disconnectSockets(opts, request.close); + } + socket = this.nsp.sockets.get(request.sid); if (!socket) { return; @@ -266,6 +291,30 @@ export class RedisAdapter extends Adapter { this.pubClient.publish(this.responseChannel, response); break; + case RequestType.REMOTE_FETCH: + if (this.requests.has(request.requestId)) { + return; + } + + const opts = { + rooms: new Set(request.opts.rooms), + except: new Set(request.opts.except), + }; + const localSockets = await super.fetchSockets(opts); + + response = JSON.stringify({ + requestId: request.requestId, + sockets: localSockets.map((socket) => ({ + id: socket.id, + handshake: socket.handshake, + rooms: [...socket.rooms], + data: socket.data, + })), + }); + + this.pubClient.publish(this.responseChannel, response); + break; + default: debug("ignoring unknown request type: %s", request.type); } @@ -299,12 +348,17 @@ export class RedisAdapter extends Adapter { switch (request.type) { case RequestType.SOCKETS: + case RequestType.REMOTE_FETCH: request.msgCount++; // ignore if response does not contain 'sockets' key if (!response.sockets || !Array.isArray(response.sockets)) return; - response.sockets.forEach((s) => request.sockets.add(s)); + if (request.type === RequestType.SOCKETS) { + response.sockets.forEach((s) => request.sockets.add(s)); + } else { + response.sockets.forEach((s) => request.sockets.push(s)); + } if (request.msgCount === request.numSub) { clearTimeout(request.timeout); @@ -587,6 +641,105 @@ export class RedisAdapter extends Adapter { }); } + public async fetchSockets(opts: BroadcastOptions): Promise { + const localSockets = await super.fetchSockets(opts); + + if (opts.flags?.local) { + return localSockets; + } + + const numSub = await this.getNumSub(); + debug('waiting for %d responses to "fetchSockets" request', numSub); + + if (numSub <= 1) { + return localSockets; + } + + const requestId = uid2(6); + + const request = JSON.stringify({ + requestId, + type: RequestType.REMOTE_FETCH, + opts: { + rooms: [...opts.rooms], + except: [...opts.except], + }, + }); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (this.requests.has(requestId)) { + reject( + new Error("timeout reached while waiting for fetchSockets response") + ); + this.requests.delete(requestId); + } + }, this.requestsTimeout); + + this.requests.set(requestId, { + type: RequestType.REMOTE_FETCH, + numSub, + resolve, + timeout, + msgCount: 1, + sockets: localSockets, + }); + + this.pubClient.publish(this.requestChannel, request); + }); + } + + public addSockets(opts: BroadcastOptions, rooms: Room[]) { + if (opts.flags?.local) { + return super.addSockets(opts, rooms); + } + + const request = JSON.stringify({ + type: RequestType.REMOTE_JOIN, + opts: { + rooms: [...opts.rooms], + except: [...opts.except], + }, + rooms: [...rooms], + }); + + this.pubClient.publish(this.requestChannel, request); + } + + public delSockets(opts: BroadcastOptions, rooms: Room[]) { + if (opts.flags?.local) { + return super.delSockets(opts, rooms); + } + + const request = JSON.stringify({ + type: RequestType.REMOTE_LEAVE, + opts: { + rooms: [...opts.rooms], + except: [...opts.except], + }, + rooms: [...rooms], + }); + + this.pubClient.publish(this.requestChannel, request); + } + + public disconnectSockets(opts: BroadcastOptions, close: boolean) { + if (opts.flags?.local) { + return super.disconnectSockets(opts, close); + } + + const request = JSON.stringify({ + type: RequestType.REMOTE_DISCONNECT, + opts: { + rooms: [...opts.rooms], + except: [...opts.except], + }, + close, + }); + + this.pubClient.publish(this.requestChannel, request); + } + /** * Get the number of subscribers of the request channel * diff --git a/package-lock.json b/package-lock.json index 2a2b4bb2..baabe5f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -248,6 +248,18 @@ "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==", "dev": true }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==", + "dev": true + }, + "@types/cors": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", + "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==", + "dev": true + }, "@types/expect.js": { "version": "0.3.29", "resolved": "https://registry.npmjs.org/@types/expect.js/-/expect.js-0.3.29.tgz", @@ -571,24 +583,35 @@ "dev": true }, "engine.io": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.2.tgz", - "integrity": "sha512-sumdttqWLNjbuSMOSgDdL2xiEld9s5QZDk9VLyr4e28o+lzNNADhU3qpQDAY7cm2VZH0Otw/U0fL8mEjZ6kBMg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz", + "integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==", "dev": true, "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", "cors": "~2.8.5", - "debug": "~4.1.0", + "debug": "~4.3.1", "engine.io-parser": "~4.0.0", - "ws": "^7.1.2" + "ws": "~7.4.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, "engine.io-client": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.2.tgz", - "integrity": "sha512-1mwvwKYMa0AaCy+sPgvJ/SnKyO5MJZ1HEeXfA3Rm/KHkHGiYD5bQVq8QzvIrkI01FuVtOdZC5lWdRw1BGXB2NQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.0.tgz", + "integrity": "sha512-e6GK0Fqvq45Nu/j7YdIVqXtDPvlsggAcfml3QiEiGdJ1qeh7IQU6knxSN3+yy9BmbnXtIfjo1hK4MFyHKdc9mQ==", "dev": true, "requires": { "base64-arraybuffer": "0.1.4", @@ -599,7 +622,6 @@ "parseqs": "0.0.6", "parseuri": "0.0.6", "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" }, "dependencies": { @@ -611,20 +633,17 @@ "requires": { "ms": "2.1.2" } - }, - "ws": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", - "dev": true } } }, "engine.io-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.1.tgz", - "integrity": "sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg==", - "dev": true + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "dev": true, + "requires": { + "base64-arraybuffer": "0.1.4" + } }, "es6-error": { "version": "4.1.1", @@ -1154,18 +1173,18 @@ "dev": true }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", + "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", "dev": true }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", + "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", "dev": true, "requires": { - "mime-db": "1.44.0" + "mime-db": "1.46.0" } }, "minimatch": { @@ -1556,43 +1575,49 @@ "dev": true }, "socket.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.1.tgz", - "integrity": "sha512-oVYbCQ4sCwm4wVi+f1bsE3YFXcvd6b4JjVP8D7IZnQqBeJOKX9XrdgJWSbXqBEqUXPY3jdTqb1M3s4KFTa/IHg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.0.tgz", + "integrity": "sha512-/c1riZMV/4yz7KEpaMhDQbwhJDIoO55whXaRKgyEBQrLU9zUHXo9rzeTMvTOqwL9mbKfHKdrXcMoCeQ/1YtMsg==", "dev": true, "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "~2.0.0", - "debug": "~4.1.0", - "engine.io": "~4.0.0", - "socket.io-adapter": "~2.0.3", - "socket.io-parser": "~4.0.1" + "debug": "~4.3.1", + "engine.io": "~5.0.0", + "socket.io-adapter": "~2.2.0", + "socket.io-parser": "~4.0.3" }, "dependencies": { - "socket.io-adapter": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", - "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==", - "dev": true + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } } } }, "socket.io-adapter": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", - "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz", + "integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg==" }, "socket.io-client": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.3.tgz", - "integrity": "sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.0.tgz", + "integrity": "sha512-27yQxmXJAEYF19Ygyl8FPJ0if0wegpSmkIIbrWJeI7n7ST1JyH8bbD5v3fjjGY5cfCanACJ3dARUAyiVFNrlTQ==", "dev": true, "requires": { "@types/component-emitter": "^1.2.10", "backo2": "~1.0.2", "component-emitter": "~1.3.0", "debug": "~4.3.1", - "engine.io-client": "~4.1.0", + "engine.io-client": "~5.0.0", "parseuri": "0.0.6", "socket.io-parser": "~4.0.4" }, @@ -1605,28 +1630,29 @@ "requires": { "ms": "2.1.2" } - }, - "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "dev": true, - "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - } } } }, "socket.io-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.1.tgz", - "integrity": "sha512-5JfNykYptCwU2lkOI0ieoePWm+6stEhkZ2UnLDjqnE1YEjUlXXLd1lpxPZ+g+h3rtaytwWkWrLQCaJULlGqjOg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", "dev": true, "requires": { + "@types/component-emitter": "^1.2.10", "component-emitter": "~1.3.0", - "debug": "~4.1.0" + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, "source-map": { @@ -1878,15 +1904,9 @@ } }, "ws": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", - "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", - "dev": true - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", "dev": true }, "y18n": { diff --git a/package.json b/package.json index 8d80efe8..2bdb70c1 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "debug": "~4.1.0", "notepack.io": "~2.2.0", "redis": "^3.0.0", - "socket.io-adapter": "~2.0.0", + "socket.io-adapter": "^2.2.0", "uid2": "0.0.3" }, "devDependencies": { @@ -34,8 +34,8 @@ "mocha": "^3.4.2", "nyc": "^15.1.0", "prettier": "^2.1.2", - "socket.io": "^3.0.1", - "socket.io-client": "^3.0.1", + "socket.io": "^4.0.0", + "socket.io-client": "^4.0.0", "ts-node": "^9.1.1", "typescript": "^4.0.5" }, diff --git a/test/index.ts b/test/index.ts index 6510346f..4fdeb9a6 100644 --- a/test/index.ts +++ b/test/index.ts @@ -5,6 +5,8 @@ import expect = require("expect.js"); import { createAdapter } from ".."; import type { AddressInfo } from "net"; +import "./util"; + const ioredis = require("ioredis").createClient; let namespace1, namespace2, namespace3; @@ -238,6 +240,111 @@ let socket1, socket2, socket3; namespace2.adapter.remoteDisconnect(socket1.id, false); }); + + describe("socketsJoin", () => { + it("makes all socket instances join the specified room", (done) => { + namespace1.socketsJoin("room1"); + setTimeout(() => { + expect(socket1.rooms).to.contain("room1"); + expect(socket2.rooms).to.contain("room1"); + expect(socket3.rooms).to.contain("room1"); + done(); + }, 100); + }); + + it("makes the matching socket instances join the specified room", (done) => { + socket1.join("room1"); + socket3.join("room1"); + namespace1.in("room1").socketsJoin("room2"); + setTimeout(() => { + expect(socket1.rooms).to.contain("room2"); + expect(socket2.rooms).to.not.contain("room2"); + expect(socket3.rooms).to.contain("room2"); + done(); + }, 100); + }); + + it("makes the given socket instance join the specified room", (done) => { + namespace1.in(socket2.id).socketsJoin("room3"); + setTimeout(() => { + expect(socket1.rooms).to.not.contain("room3"); + expect(socket2.rooms).to.contain("room3"); + expect(socket3.rooms).to.not.contain("room3"); + done(); + }, 100); + }); + }); + + describe("socketsLeave", () => { + it("makes all socket instances leave the specified room", (done) => { + socket2.join("room1"); + socket3.join("room1"); + namespace1.socketsLeave("room1"); + setTimeout(() => { + expect(socket1.rooms).to.not.contain("room1"); + expect(socket2.rooms).to.not.contain("room1"); + expect(socket3.rooms).to.not.contain("room1"); + done(); + }, 100); + }); + + it("makes the matching socket instances leave the specified room", (done) => { + socket1.join(["room1", "room2"]); + socket2.join(["room1", "room2"]); + socket3.join(["room2"]); + namespace1.in("room1").socketsLeave("room2"); + setTimeout(() => { + expect(socket1.rooms).to.not.contain("room2"); + expect(socket2.rooms).to.not.contain("room2"); + expect(socket3.rooms).to.contain("room2"); + done(); + }, 100); + }); + + it("makes the given socket instance leave the specified room", (done) => { + socket1.join("room3"); + socket2.join("room3"); + socket3.join("room3"); + namespace1.in(socket2.id).socketsLeave("room3"); + setTimeout(() => { + expect(socket1.rooms).to.contain("room3"); + expect(socket2.rooms).to.not.contain("room3"); + expect(socket3.rooms).to.contain("room3"); + done(); + }, 100); + }); + }); + + describe("fetchSockets", () => { + it("returns all socket instances", async () => { + socket2.data = "test"; + + const sockets = await namespace1.fetchSockets(); + expect(sockets).to.be.an(Array); + expect(sockets).to.have.length(3); + const remoteSocket1 = sockets.find( + (socket) => socket.id === socket1.id + ); + expect(remoteSocket1 === socket1).to.be(true); + const remoteSocket2 = sockets.find( + (socket) => socket.id === socket2.id + ); + expect(remoteSocket2 === socket2).to.be(false); + expect(remoteSocket2.handshake).to.eql(socket2.handshake); + expect(remoteSocket2.data).to.eql("test"); + expect(remoteSocket2.rooms.size).to.eql(1); + expect(remoteSocket2.rooms).to.contain(socket2.id); + }); + + it("returns the matching socket instances", async () => { + socket1.join("room1"); + socket3.join("room1"); + + const sockets = await namespace1.in("room1").fetchSockets(); + expect(sockets).to.be.an(Array); + expect(sockets).to.have.length(2); + }); + }); }); }); }); diff --git a/test/util.ts b/test/util.ts new file mode 100644 index 00000000..9f91d32d --- /dev/null +++ b/test/util.ts @@ -0,0 +1,22 @@ +// @ts-ignore +import { Assertion, stringify as i } from "expect.js"; + +// add support for Set/Map +const contain = Assertion.prototype.contain; +Assertion.prototype.contain = function (...args) { + if (typeof this.obj === "object") { + args.forEach((obj) => { + this.assert( + this.obj.has(obj), + function () { + return "expected " + i(this.obj) + " to contain " + i(obj); + }, + function () { + return "expected " + i(this.obj) + " to not contain " + i(obj); + } + ); + }); + return this; + } + return contain.apply(this, args); +};