Skip to content

Commit

Permalink
feat: implement connection state recovery
Browse files Browse the repository at this point in the history
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
  • Loading branch information
darrachequesne committed Jan 25, 2023
1 parent a1c528b commit b4e20c5
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 44 deletions.
42 changes: 37 additions & 5 deletions lib/socket.ts
Expand Up @@ -130,6 +130,20 @@ export class Socket<
*/
public id: string;

/**
* The session ID used for connection state recovery, which must not be shared (unlike {@link id}).
*
* @private
*/
private _pid: string;

/**
* The offset of the last received packet, which will be sent upon reconnection to allow for the recovery of the connection state.
*
* @private
*/
private _lastOffset: string;

/**
* Whether the socket is currently connected to the server.
*
Expand Down Expand Up @@ -413,13 +427,28 @@ export class Socket<
debug("transport is open - connecting");
if (typeof this.auth == "function") {
this.auth((data) => {
this.packet({ type: PacketType.CONNECT, data });
this._sendConnectPacket(data as Record<string, unknown>);
});
} else {
this.packet({ type: PacketType.CONNECT, data: this.auth });
this._sendConnectPacket(this.auth);
}
}

/**
* Sends a CONNECT packet to initiate the Socket.IO session.
*
* @param data
* @private
*/
private _sendConnectPacket(data: Record<string, unknown>) {
this.packet({
type: PacketType.CONNECT,
data: this._pid
? Object.assign({ pid: this._pid, offset: this._lastOffset }, data)
: data,
});
}

/**
* Called upon engine or manager `error`.
*
Expand Down Expand Up @@ -463,8 +492,7 @@ export class Socket<
switch (packet.type) {
case PacketType.CONNECT:
if (packet.data && packet.data.sid) {
const id = packet.data.sid;
this.onconnect(id);
this.onconnect(packet.data.sid, packet.data.pid);
} else {
this.emitReserved(
"connect_error",
Expand Down Expand Up @@ -529,6 +557,9 @@ export class Socket<
}
}
super.emit.apply(this, args);
if (this._pid && args.length && typeof args[args.length - 1] === "string") {
this._lastOffset = args[args.length - 1];
}
}

/**
Expand Down Expand Up @@ -575,9 +606,10 @@ export class Socket<
*
* @private
*/
private onconnect(id: string): void {
private onconnect(id: string, pid: string) {
debug("socket connected with id %s", id);
this.id = id;
this._pid = pid; // defined only if connection state recovery is enabled
this.connected = true;
this.emitBuffered();
this.emitReserved("connect");
Expand Down
146 changes: 109 additions & 37 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -66,7 +66,7 @@
"rimraf": "^3.0.2",
"rollup": "^2.58.0",
"rollup-plugin-terser": "^7.0.2",
"socket.io": "^4.5.3",
"socket.io": "^4.6.0-alpha1",
"socket.io-msgpack-parser": "^3.0.0",
"text-blob-builder": "0.0.1",
"ts-loader": "^8.3.0",
Expand Down
26 changes: 26 additions & 0 deletions test/connection-state-recovery.ts
@@ -0,0 +1,26 @@
import expect from "expect.js";
import { io } from "..";
import { wrap, BASE_URL, success } from "./support/util";

describe("connection state recovery", () => {
it("should have an accessible socket id equal to the server-side socket id (default namespace)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
});

socket.emit("hi");

socket.on("hi", () => {
const id = socket.id;

socket.io.engine.close();

socket.on("connect", () => {
expect(socket.id).to.eql(id); // means that the reconnection was successful
done();
});
});
});
});
});
1 change: 1 addition & 0 deletions test/index.ts
Expand Up @@ -2,3 +2,4 @@ import "./url";
import "./connection";
import "./socket";
import "./node";
import "./connection-state-recovery";
5 changes: 4 additions & 1 deletion test/support/server.ts
Expand Up @@ -2,7 +2,10 @@ import { Server } from "socket.io";
import expect from "expect.js";

export function createServer() {
const server = new Server(3210, { pingInterval: 2000 });
const server = new Server(3210, {
pingInterval: 2000,
connectionStateRecovery: {},
});

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

0 comments on commit b4e20c5

Please sign in to comment.