Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions packages/socket.io-adapter/lib/in-memory-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Session> {
return null;
}
Expand Down Expand Up @@ -444,7 +444,7 @@ export class SessionAwareAdapter extends Adapter {

override restoreSession(
pid: PrivateSessionId,
offset: string,
offset?: string,
): Promise<Session> {
const session = this.sessions.get(pid);
if (!session) {
Expand All @@ -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({
Expand Down
43 changes: 43 additions & 0 deletions packages/socket.io-adapter/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});
});
26 changes: 26 additions & 0 deletions packages/socket.io-client/test/connection-state-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});
});
});
6 changes: 3 additions & 3 deletions packages/socket.io/lib/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,12 @@ export class Namespace<
auth: Record<string, unknown>,
) {
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 {
Expand Down
87 changes: 87 additions & 0 deletions packages/socket.io/test/connection-state-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Socket>(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<Socket>(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, {
Expand Down