Skip to content

Commit b4e20c5

Browse files
feat: implement connection state recovery
Connection state recovery allows a client to reconnect after a temporary disconnection and restore its state: - id - rooms - data - missed packets See also: socketio/socket.io@54d5ee0
1 parent a1c528b commit b4e20c5

File tree

6 files changed

+178
-44
lines changed

6 files changed

+178
-44
lines changed

lib/socket.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ export class Socket<
130130
*/
131131
public id: string;
132132

133+
/**
134+
* The session ID used for connection state recovery, which must not be shared (unlike {@link id}).
135+
*
136+
* @private
137+
*/
138+
private _pid: string;
139+
140+
/**
141+
* The offset of the last received packet, which will be sent upon reconnection to allow for the recovery of the connection state.
142+
*
143+
* @private
144+
*/
145+
private _lastOffset: string;
146+
133147
/**
134148
* Whether the socket is currently connected to the server.
135149
*
@@ -413,13 +427,28 @@ export class Socket<
413427
debug("transport is open - connecting");
414428
if (typeof this.auth == "function") {
415429
this.auth((data) => {
416-
this.packet({ type: PacketType.CONNECT, data });
430+
this._sendConnectPacket(data as Record<string, unknown>);
417431
});
418432
} else {
419-
this.packet({ type: PacketType.CONNECT, data: this.auth });
433+
this._sendConnectPacket(this.auth);
420434
}
421435
}
422436

437+
/**
438+
* Sends a CONNECT packet to initiate the Socket.IO session.
439+
*
440+
* @param data
441+
* @private
442+
*/
443+
private _sendConnectPacket(data: Record<string, unknown>) {
444+
this.packet({
445+
type: PacketType.CONNECT,
446+
data: this._pid
447+
? Object.assign({ pid: this._pid, offset: this._lastOffset }, data)
448+
: data,
449+
});
450+
}
451+
423452
/**
424453
* Called upon engine or manager `error`.
425454
*
@@ -463,8 +492,7 @@ export class Socket<
463492
switch (packet.type) {
464493
case PacketType.CONNECT:
465494
if (packet.data && packet.data.sid) {
466-
const id = packet.data.sid;
467-
this.onconnect(id);
495+
this.onconnect(packet.data.sid, packet.data.pid);
468496
} else {
469497
this.emitReserved(
470498
"connect_error",
@@ -529,6 +557,9 @@ export class Socket<
529557
}
530558
}
531559
super.emit.apply(this, args);
560+
if (this._pid && args.length && typeof args[args.length - 1] === "string") {
561+
this._lastOffset = args[args.length - 1];
562+
}
532563
}
533564

534565
/**
@@ -575,9 +606,10 @@ export class Socket<
575606
*
576607
* @private
577608
*/
578-
private onconnect(id: string): void {
609+
private onconnect(id: string, pid: string) {
579610
debug("socket connected with id %s", id);
580611
this.id = id;
612+
this._pid = pid; // defined only if connection state recovery is enabled
581613
this.connected = true;
582614
this.emitBuffered();
583615
this.emitReserved("connect");

package-lock.json

Lines changed: 109 additions & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"rimraf": "^3.0.2",
6767
"rollup": "^2.58.0",
6868
"rollup-plugin-terser": "^7.0.2",
69-
"socket.io": "^4.5.3",
69+
"socket.io": "^4.6.0-alpha1",
7070
"socket.io-msgpack-parser": "^3.0.0",
7171
"text-blob-builder": "0.0.1",
7272
"ts-loader": "^8.3.0",

test/connection-state-recovery.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import expect from "expect.js";
2+
import { io } from "..";
3+
import { wrap, BASE_URL, success } from "./support/util";
4+
5+
describe("connection state recovery", () => {
6+
it("should have an accessible socket id equal to the server-side socket id (default namespace)", () => {
7+
return wrap((done) => {
8+
const socket = io(BASE_URL, {
9+
forceNew: true,
10+
});
11+
12+
socket.emit("hi");
13+
14+
socket.on("hi", () => {
15+
const id = socket.id;
16+
17+
socket.io.engine.close();
18+
19+
socket.on("connect", () => {
20+
expect(socket.id).to.eql(id); // means that the reconnection was successful
21+
done();
22+
});
23+
});
24+
});
25+
});
26+
});

test/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import "./url";
22
import "./connection";
33
import "./socket";
44
import "./node";
5+
import "./connection-state-recovery";

test/support/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Server } from "socket.io";
22
import expect from "expect.js";
33

44
export function createServer() {
5-
const server = new Server(3210, { pingInterval: 2000 });
5+
const server = new Server(3210, {
6+
pingInterval: 2000,
7+
connectionStateRecovery: {},
8+
});
69

710
server.of("/foo").on("connection", (socket) => {
811
socket.on("getId", (cb) => {

0 commit comments

Comments
 (0)