From ebdba472da0f8dca6c65858ed9f9e9e4304eefe3 Mon Sep 17 00:00:00 2001 From: chuanghiduoc Date: Sat, 22 Aug 2026 23:19:54 +0700 Subject: [PATCH] fix(adapter): do not let volatile packets break connection state recovery The client stores the offset of every received event: it takes the last item of the data array (see emitEvent() in the client). Volatile EVENT packets did not get an offset appended, so if such a packet ended with a string argument, the client stored that string as its offset. The next restoreSession() call then failed to find it and the whole session was dropped, silently discarding every packet emitted during the outage. Volatile packets are now part of the offset chain like any other EVENT packet, but they are still excluded from the replay in restoreSession(), so their semantics are unchanged. --- .../lib/in-memory-adapter.ts | 13 ++-- packages/socket.io-adapter/test/index.ts | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/packages/socket.io-adapter/lib/in-memory-adapter.ts b/packages/socket.io-adapter/lib/in-memory-adapter.ts index cf178170e0..76b6bcd2da 100644 --- a/packages/socket.io-adapter/lib/in-memory-adapter.ts +++ b/packages/socket.io-adapter/lib/in-memory-adapter.ts @@ -466,7 +466,9 @@ export class SessionAwareAdapter extends Adapter { const missedPackets = []; for (let i = index + 1; i < this.packets.length; i++) { const packet = this.packets[i]; - if (shouldIncludePacket(session.rooms, packet.opts)) { + // volatile packets are part of the offset chain but are never replayed + const isVolatile = packet.opts.flags?.volatile !== undefined; + if (!isVolatile && shouldIncludePacket(session.rooms, packet.opts)) { missedPackets.push(packet.data); } } @@ -478,15 +480,16 @@ export class SessionAwareAdapter extends Adapter { override broadcast(packet: any, opts: BroadcastOptions) { const isEventPacket = packet.type === 2; - // packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and - // restored on another server upon reconnection + // packets with acknowledgement are not stored because the acknowledgement function cannot be + // serialized and restored on another server upon reconnection const withoutAcknowledgement = packet.id === undefined; - const notVolatile = opts.flags?.volatile === undefined; - if (isEventPacket && withoutAcknowledgement && notVolatile) { + if (isEventPacket && withoutAcknowledgement) { const id = yeast(); // the offset is stored at the end of the data array, so the client knows the ID of the last packet it has // processed (and the format is backward-compatible) packet.data.push(id); + // volatile packets are included in the offset chain so that the client always sends a known + // offset upon reconnection, but they are excluded from the replay in restoreSession() this.packets.push({ id, opts, diff --git a/packages/socket.io-adapter/test/index.ts b/packages/socket.io-adapter/test/index.ts index 285ae66f02..cd26ce6e73 100644 --- a/packages/socket.io-adapter/test/index.ts +++ b/packages/socket.io-adapter/test/index.ts @@ -551,5 +551,67 @@ describe("socket.io-adapter", () => { expect(session).to.be(null); }); + + it("should still restore a session after the client received a volatile packet", async () => { + const adapter = new SessionAwareAdapter({ + server: { + encoder: { + encode(packet) { + return packet; + }, + }, + opts: { + connectionStateRecovery: { + maxDisconnectionDuration: 5000, + }, + }, + }, + }); + + adapter.persistSession({ + sid: "abc", + pid: "def", + data: "ghi", + rooms: ["r1"], + }); + + const packetData = ["hello"]; + + adapter.broadcast( + { + nsp: "/", + type: 2, + data: packetData, + }, + { + rooms: new Set(), + except: new Set(), + }, + ); + + const volatileData = ["status", "connected"]; + + adapter.broadcast( + { + nsp: "/", + type: 2, + data: volatileData, + }, + { + rooms: new Set(), + except: new Set(), + flags: { + volatile: true, + }, + }, + ); + + // the adapter appends its own offset at the end of the data array, so the client stores + // that offset instead of the last argument of the event itself + const session = await adapter.restoreSession("def", volatileData[2]); + + expect(session).to.not.be(null); + expect(session.missedPackets.length).to.eql(0); + }); }); });