diff --git a/packages/socket.io-adapter/lib/in-memory-adapter.ts b/packages/socket.io-adapter/lib/in-memory-adapter.ts index cf178170e0..07aee0de26 100644 --- a/packages/socket.io-adapter/lib/in-memory-adapter.ts +++ b/packages/socket.io-adapter/lib/in-memory-adapter.ts @@ -387,11 +387,11 @@ export class Adapter extends EventEmitter { /** * Restore the session and find the packets that were missed by the client. * @param pid - * @param offset + * @param offset - the offset of the last packet received by the client, if any */ public restoreSession( pid: PrivateSessionId, - offset: string, + offset?: string, ): Promise { return null; } @@ -444,7 +444,7 @@ export class SessionAwareAdapter extends Adapter { override restoreSession( pid: PrivateSessionId, - offset: string, + offset?: string, ): Promise { const session = this.sessions.get(pid); if (!session) { @@ -458,16 +458,18 @@ export class SessionAwareAdapter extends Adapter { this.sessions.delete(pid); return null; } - const index = this.packets.findIndex((packet) => packet.id === offset); - if (index === -1) { - // the offset may be too old - return null; - } const missedPackets = []; - for (let i = index + 1; i < this.packets.length; i++) { - const packet = this.packets[i]; - if (shouldIncludePacket(session.rooms, packet.opts)) { - missedPackets.push(packet.data); + if (offset !== undefined) { + const index = this.packets.findIndex((packet) => packet.id === offset); + if (index === -1) { + // the offset may be too old + return null; + } + for (let i = index + 1; i < this.packets.length; i++) { + const packet = this.packets[i]; + if (shouldIncludePacket(session.rooms, packet.opts)) { + missedPackets.push(packet.data); + } } } return Promise.resolve({ diff --git a/packages/socket.io-adapter/test/index.ts b/packages/socket.io-adapter/test/index.ts index 285ae66f02..96ff3881ad 100644 --- a/packages/socket.io-adapter/test/index.ts +++ b/packages/socket.io-adapter/test/index.ts @@ -551,5 +551,48 @@ describe("socket.io-adapter", () => { expect(session).to.be(null); }); + + it("should restore a known session without offset", 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", "r2"], + }); + + adapter.broadcast( + { + nsp: "/", + type: 2, + data: ["hello"], + }, + { + rooms: new Set(), + except: new Set(), + }, + ); + + const session = await adapter.restoreSession("def"); + + expect(session).to.not.be(null); + expect(session.sid).to.eql("abc"); + expect(session.pid).to.eql("def"); + expect(session.missedPackets).to.eql([]); + }); }); }); diff --git a/packages/socket.io-client/test/connection-state-recovery.ts b/packages/socket.io-client/test/connection-state-recovery.ts index 42c826b0ea..04dcc69f6f 100644 --- a/packages/socket.io-client/test/connection-state-recovery.ts +++ b/packages/socket.io-client/test/connection-state-recovery.ts @@ -27,4 +27,30 @@ describe("connection state recovery", () => { }); }); }); + + it("should restore session even if no event was received", () => { + return wrap((done) => { + const socket = io(BASE_URL, { + forceNew: true, + reconnectionDelay: 10, + }); + + expect(socket.recovered).to.eql(false); + + let id: string; + + socket.on("connect", () => { + if (!id) { + // first connection: no event has been exchanged yet + id = socket.id; + + socket.io.engine.close(); + } else { + expect(socket.id).to.eql(id); // means that the reconnection was successful + expect(socket.recovered).to.eql(true); // means that the reconnection was successful + done(); + } + }); + }); + }); }); diff --git a/packages/socket.io/lib/namespace.ts b/packages/socket.io/lib/namespace.ts index 72df7301e3..f38dbc8178 100644 --- a/packages/socket.io/lib/namespace.ts +++ b/packages/socket.io/lib/namespace.ts @@ -384,12 +384,12 @@ export class Namespace< auth: Record, ) { const sessionId = auth.pid; - const offset = auth.offset; + // note: the offset may be undefined, if the client has not yet received any event + const offset = typeof auth.offset === "string" ? auth.offset : undefined; if ( // @ts-ignore this.server.opts.connectionStateRecovery && - typeof sessionId === "string" && - typeof offset === "string" + typeof sessionId === "string" ) { let session; try { diff --git a/packages/socket.io/test/connection-state-recovery.ts b/packages/socket.io/test/connection-state-recovery.ts index c0dcbf0703..a3528425a2 100644 --- a/packages/socket.io/test/connection-state-recovery.ts +++ b/packages/socket.io/test/connection-state-recovery.ts @@ -113,6 +113,93 @@ describe("connection state recovery", () => { io.close(); }); + it("should restore session even if the client did not receive any event", async () => { + const httpServer = createServer().listen(0); + const io = new Server(httpServer, { + connectionStateRecovery: {}, + }); + + io.once("connection", (socket) => { + expect(socket.recovered).to.eql(false); + + socket.join("room1"); + socket.data.foo = "bar"; + }); + + // Engine.IO handshake + const eioSid = await eioHandshake(httpServer); + + // Socket.IO handshake (without any prior event, hence without any offset) + await eioPush(httpServer, eioSid, "40"); + const handshakeBody = await eioPoll(httpServer, eioSid); + + expect(handshakeBody.startsWith("40")).to.be(true); + + const handshake = JSON.parse(handshakeBody.substring(2)); + + expect(handshake.sid).to.not.be(undefined); + expect(handshake.pid).to.not.be(undefined); + + await eioPush(httpServer, eioSid, "1"); // close + + const newSid = await eioHandshake(httpServer); + + const [socket] = await Promise.all([ + waitFor(io, "connection"), + eioPush(httpServer, newSid, `40{"pid":"${handshake.pid}"}`), + ]); + + expect(socket.id).to.eql(handshake.sid); + expect(socket.recovered).to.eql(true); + + expect(socket.rooms.has(socket.id)).to.eql(true); + expect(socket.rooms.has("room1")).to.eql(true); + + expect(socket.data.foo).to.eql("bar"); + + const payload = await eioPoll(httpServer, newSid); + expect(payload).to.eql( + `40{"sid":"${handshake.sid}","pid":"${handshake.pid}"}`, + ); + + io.close(); + }); + + it("should restore session even if the provided offset is not a string", async () => { + const httpServer = createServer().listen(0); + const io = new Server(httpServer, { + connectionStateRecovery: {}, + }); + + io.once("connection", (socket) => { + socket.join("room1"); + }); + + // Engine.IO handshake + const eioSid = await eioHandshake(httpServer); + + // Socket.IO handshake + await eioPush(httpServer, eioSid, "40"); + const handshakeBody = await eioPoll(httpServer, eioSid); + const handshake = JSON.parse(handshakeBody.substring(2)); + + await eioPush(httpServer, eioSid, "1"); // close + + const newSid = await eioHandshake(httpServer); + + const [socket] = await Promise.all([ + waitFor(io, "connection"), + eioPush(httpServer, newSid, `40{"pid":"${handshake.pid}","offset":123}`), + ]); + + expect(socket.id).to.eql(handshake.sid); + expect(socket.recovered).to.eql(true); + expect(socket.rooms.has("room1")).to.eql(true); + + await eioPoll(httpServer, newSid); // drain buffer + io.close(); + }); + it("should not run middlewares upon recovery by default", async () => { const httpServer = createServer().listen(0); const io = new Server(httpServer, {