From ca397f3afe06ed9390db52b70a506a9721e091d8 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 07:54:48 +0100 Subject: [PATCH 1/7] fix(types): ensure compatibility with TypeScript < 4.5 "import { type ... }" was added in TypeScript 4.5. Reference: https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/ Related: - https://github.com/socketio/socket.io-adapter/issues/86 - https://github.com/socketio/socket.io/issues/3891 --- lib/cluster-adapter.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/cluster-adapter.ts b/lib/cluster-adapter.ts index 8858d0e..8ade055 100644 --- a/lib/cluster-adapter.ts +++ b/lib/cluster-adapter.ts @@ -1,9 +1,5 @@ -import { - Adapter, - type BroadcastFlags, - type BroadcastOptions, - type Room, -} from "./index"; +import { Adapter } from "./index"; +import type { BroadcastFlags, BroadcastOptions, Room } from "./index"; import { debug as debugModule } from "debug"; import { randomBytes } from "crypto"; From 9d4c4a75a498e6766a06bffb02c0e5253b8d4efd Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 08:01:30 +0100 Subject: [PATCH 2/7] refactor(cluster): export ClusterAdapterOptions and MessageType types Related: - https://github.com/socketio/socket.io-redis-streams-adapter/blob/89d00a49e4aadf1bfe32ddc644d0ef877a08238d/lib/adapter.ts#L17 - https://github.com/socketio/socket.io-redis-streams-adapter/blob/89d00a49e4aadf1bfe32ddc644d0ef877a08238d/lib/adapter.ts#L191 --- lib/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/index.ts b/lib/index.ts index 7743723..acb4248 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -509,8 +509,10 @@ function shouldIncludePacket( export { ClusterAdapter, ClusterAdapterWithHeartbeat, + ClusterAdapterOptions, ClusterMessage, ClusterResponse, + MessageType, ServerId, Offset, } from "./cluster-adapter"; From abc93a9ac75e1dd0258349af10a0ad6e4617da5d Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 08:32:40 +0100 Subject: [PATCH 3/7] refactor: break circular dependency (1) This will be done in two steps, in order to preserve the history of the index.ts file. --- lib/cluster-adapter.ts | 8 ++++++-- lib/{index.ts => in-memory-adapter.ts} | 0 2 files changed, 6 insertions(+), 2 deletions(-) rename lib/{index.ts => in-memory-adapter.ts} (100%) diff --git a/lib/cluster-adapter.ts b/lib/cluster-adapter.ts index 8ade055..cf6baa1 100644 --- a/lib/cluster-adapter.ts +++ b/lib/cluster-adapter.ts @@ -1,5 +1,9 @@ -import { Adapter } from "./index"; -import type { BroadcastFlags, BroadcastOptions, Room } from "./index"; +import { Adapter } from "./in-memory-adapter"; +import type { + BroadcastFlags, + BroadcastOptions, + Room, +} from "./in-memory-adapter"; import { debug as debugModule } from "debug"; import { randomBytes } from "crypto"; diff --git a/lib/index.ts b/lib/in-memory-adapter.ts similarity index 100% rename from lib/index.ts rename to lib/in-memory-adapter.ts From 207c0dba1af9d929fb076b5bd60253adc778c98a Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 08:34:29 +0100 Subject: [PATCH 4/7] refactor: break circular dependency (2) --- lib/in-memory-adapter.ts | 11 ----------- lib/index.ts | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 lib/index.ts diff --git a/lib/in-memory-adapter.ts b/lib/in-memory-adapter.ts index acb4248..83d4d67 100644 --- a/lib/in-memory-adapter.ts +++ b/lib/in-memory-adapter.ts @@ -505,14 +505,3 @@ function shouldIncludePacket( const notExcluded = sessionRooms.every((room) => !opts.except.has(room)); return included && notExcluded; } - -export { - ClusterAdapter, - ClusterAdapterWithHeartbeat, - ClusterAdapterOptions, - ClusterMessage, - ClusterResponse, - MessageType, - ServerId, - Offset, -} from "./cluster-adapter"; diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..2665139 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,21 @@ +export { + SocketId, + PrivateSessionId, + Room, + BroadcastFlags, + BroadcastOptions, + Session, + Adapter, + SessionAwareAdapter, +} from "./in-memory-adapter"; + +export { + ClusterAdapter, + ClusterAdapterWithHeartbeat, + ClusterAdapterOptions, + ClusterMessage, + ClusterResponse, + MessageType, + ServerId, + Offset, +} from "./cluster-adapter"; From a13f35f0e6b85bbba07f99ee2440e914f1429d83 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 08:57:49 +0100 Subject: [PATCH 5/7] fix: ensure the order of the commands Before this change, the broadcast() method would send the BROADCAST command and then apply it locally (which is required to retrieve the offset of the message, when connection state recovery is enabled), while the other commands like disconnectSockets() would first apply it locally and then send the command to the other nodes. So, for example: ```js io.emit("bye"); io.disconnectSockets(); ``` In that case, the clients connected to the io instance would not receive the "bye" event, while the clients connected to the other server instances would receive it before being disconnected. Related: - https://github.com/socketio/socket.io-redis-streams-adapter/issues/13 - https://github.com/socketio/socket.io-postgres-adapter/issues/12 --- lib/cluster-adapter.ts | 81 +++++++++++++++++++++++------------------ test/cluster-adapter.ts | 26 ++++++++++++- test/util.ts | 4 ++ 3 files changed, 74 insertions(+), 37 deletions(-) diff --git a/lib/cluster-adapter.ts b/lib/cluster-adapter.ts index cf6baa1..143bb6f 100644 --- a/lib/cluster-adapter.ts +++ b/lib/cluster-adapter.ts @@ -503,55 +503,64 @@ export abstract class ClusterAdapter extends Adapter { super.broadcastWithAck(packet, opts, clientCountCallback, ack); } - override addSockets(opts: BroadcastOptions, rooms: Room[]) { - super.addSockets(opts, rooms); - + override async addSockets(opts: BroadcastOptions, rooms: Room[]) { const onlyLocal = opts.flags?.local; - if (onlyLocal) { - return; + + if (!onlyLocal) { + try { + await this.publishAndReturnOffset({ + type: MessageType.SOCKETS_JOIN, + data: { + opts: encodeOptions(opts), + rooms, + }, + }); + } catch (e) { + debug("[%s] error while publishing message: %s", this.uid, e.message); + } } - this.publish({ - type: MessageType.SOCKETS_JOIN, - data: { - opts: encodeOptions(opts), - rooms, - }, - }); + super.addSockets(opts, rooms); } - override delSockets(opts: BroadcastOptions, rooms: Room[]) { - super.delSockets(opts, rooms); - + override async delSockets(opts: BroadcastOptions, rooms: Room[]) { const onlyLocal = opts.flags?.local; - if (onlyLocal) { - return; + + if (!onlyLocal) { + try { + await this.publishAndReturnOffset({ + type: MessageType.SOCKETS_LEAVE, + data: { + opts: encodeOptions(opts), + rooms, + }, + }); + } catch (e) { + debug("[%s] error while publishing message: %s", this.uid, e.message); + } } - this.publish({ - type: MessageType.SOCKETS_LEAVE, - data: { - opts: encodeOptions(opts), - rooms, - }, - }); + super.delSockets(opts, rooms); } - override disconnectSockets(opts: BroadcastOptions, close: boolean) { - super.disconnectSockets(opts, close); - + override async disconnectSockets(opts: BroadcastOptions, close: boolean) { const onlyLocal = opts.flags?.local; - if (onlyLocal) { - return; + + if (!onlyLocal) { + try { + await this.publishAndReturnOffset({ + type: MessageType.DISCONNECT_SOCKETS, + data: { + opts: encodeOptions(opts), + close, + }, + }); + } catch (e) { + debug("[%s] error while publishing message: %s", this.uid, e.message); + } } - this.publish({ - type: MessageType.DISCONNECT_SOCKETS, - data: { - opts: encodeOptions(opts), - close, - }, - }); + super.disconnectSockets(opts, close); } async fetchSockets(opts: BroadcastOptions): Promise { diff --git a/test/cluster-adapter.ts b/test/cluster-adapter.ts index d5d6fdb..350ecda 100644 --- a/test/cluster-adapter.ts +++ b/test/cluster-adapter.ts @@ -3,7 +3,7 @@ import { Server, Socket as ServerSocket } from "socket.io"; import { io as ioc, Socket as ClientSocket } from "socket.io-client"; import expect = require("expect.js"); import type { AddressInfo } from "net"; -import { times, shouldNotHappen } from "./util"; +import { times, shouldNotHappen, sleep } from "./util"; import { ClusterAdapterWithHeartbeat, type ClusterMessage, @@ -243,6 +243,8 @@ describe("cluster adapter", () => { it("makes all socket instances join the specified room", async () => { servers[0].socketsJoin("room1"); + await sleep(); + expect(serverSockets[0].rooms.has("room1")).to.be(true); expect(serverSockets[1].rooms.has("room1")).to.be(true); expect(serverSockets[2].rooms.has("room1")).to.be(true); @@ -254,6 +256,8 @@ describe("cluster adapter", () => { servers[0].in("room1").socketsJoin("room2"); + await sleep(); + expect(serverSockets[0].rooms.has("room2")).to.be(true); expect(serverSockets[1].rooms.has("room2")).to.be(false); expect(serverSockets[2].rooms.has("room2")).to.be(true); @@ -275,6 +279,8 @@ describe("cluster adapter", () => { servers[0].socketsLeave("room1"); + await sleep(); + expect(serverSockets[0].rooms.has("room1")).to.be(false); expect(serverSockets[1].rooms.has("room1")).to.be(false); expect(serverSockets[2].rooms.has("room1")).to.be(false); @@ -287,6 +293,8 @@ describe("cluster adapter", () => { servers[0].in("room1").socketsLeave("room2"); + await sleep(); + expect(serverSockets[0].rooms.has("room2")).to.be(false); expect(serverSockets[1].rooms.has("room2")).to.be(false); expect(serverSockets[2].rooms.has("room2")).to.be(true); @@ -318,6 +326,22 @@ describe("cluster adapter", () => { servers[0].disconnectSockets(true); }); + + it("sends a packet before all socket instances disconnect", (done) => { + const partialDone = times(3, done); + + clientSockets.forEach((clientSocket) => { + clientSocket.on("disconnect", shouldNotHappen(done)); + + clientSocket.on("bye", () => { + clientSocket.off("disconnect"); + clientSocket.on("disconnect", partialDone); + }); + }); + + servers[0].emit("bye"); + servers[0].disconnectSockets(true); + }); }); describe("fetchSockets", () => { diff --git a/test/util.ts b/test/util.ts index 8109ef1..ecdf6df 100644 --- a/test/util.ts +++ b/test/util.ts @@ -13,3 +13,7 @@ export function times(count: number, fn: () => void) { export function shouldNotHappen(done) { return () => done(new Error("should not happen")); } + +export function sleep() { + return new Promise((resolve) => process.nextTick(resolve)); +} From 005d546767508227309b01c75a47c006ccb46f26 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 09:26:50 +0100 Subject: [PATCH 6/7] ci: test with older TypeScript version --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d69694..da08b0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: strategy: matrix: node-version: + - 14 - 20 steps: @@ -31,5 +32,11 @@ jobs: - name: Install dependencies run: npm ci + # the "override" keyword was added in typescript@4.5.0 + # else, users can go down to typescript@3.8.x ("import type") + - name: Install TypeScript 4.5 + run: npm i typescript@4.5 + if: ${{ matrix.node-version == '14' }} + - name: Run tests run: npm test From 5eae5a0b541b2d671b561e88029f8aa888cb33f9 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 22 Feb 2024 09:33:01 +0100 Subject: [PATCH 7/7] chore(release): 2.5.4 Diff: https://github.com/socketio/socket.io-adapter/compare/2.5.3...2.5.4 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e5447a..c06c36a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # History +- [2.5.4](#254-2024-02-22) (Feb 2024) - [2.5.3](#253-2024-02-21) (Feb 2024) - [2.5.2](#252-2023-01-12) (Jan 2023) - [2.5.1](#251-2023-01-06) (Jan 2023) @@ -20,6 +21,16 @@ # Release notes +## [2.5.4](https://github.com/socketio/socket.io-adapter/compare/2.5.3...2.5.4) (2024-02-22) + + +### Bug Fixes + +* ensure the order of the commands ([a13f35f](https://github.com/socketio/socket.io-adapter/commit/a13f35f0e6b85bbba07f99ee2440e914f1429d83)) +* **types:** ensure compatibility with TypeScript < 4.5 ([ca397f3](https://github.com/socketio/socket.io-adapter/commit/ca397f3afe06ed9390db52b70a506a9721e091d8)) + + + ## [2.5.3](https://github.com/socketio/socket.io-adapter/compare/2.5.2...2.5.3) (2024-02-21) Two abstract classes were imported from the [Redis adapter repository](https://github.com/socketio/socket.io-redis-adapter/blob/bd32763043a2eb79a21dffd8820f20e598348adf/lib/cluster-adapter.ts): diff --git a/package.json b/package.json index 081d9fb..bd165e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket.io-adapter", - "version": "2.5.3", + "version": "2.5.4", "license": "MIT", "repository": { "type": "git",