From b4e20c5c709b5e9cc03ee9b6bd1d576f4810a817 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Wed, 25 Jan 2023 09:21:08 +0100 Subject: [PATCH] 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: https://github.com/socketio/socket.io/commit/54d5ee05a684371191e207b8089f09fc24eb5107 --- lib/socket.ts | 42 ++++++++- package-lock.json | 146 ++++++++++++++++++++++-------- package.json | 2 +- test/connection-state-recovery.ts | 26 ++++++ test/index.ts | 1 + test/support/server.ts | 5 +- 6 files changed, 178 insertions(+), 44 deletions(-) create mode 100644 test/connection-state-recovery.ts diff --git a/lib/socket.ts b/lib/socket.ts index a7faaf1d..b4df804f 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -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. * @@ -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); }); } 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) { + this.packet({ + type: PacketType.CONNECT, + data: this._pid + ? Object.assign({ pid: this._pid, offset: this._lastOffset }, data) + : data, + }); + } + /** * Called upon engine or manager `error`. * @@ -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", @@ -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]; + } } /** @@ -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"); diff --git a/package-lock.json b/package-lock.json index c0393e27..89238c49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,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", @@ -2299,10 +2299,13 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/diff": { "version": "5.0.2", @@ -6071,9 +6074,9 @@ } }, "node_modules/engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.3.1.tgz", + "integrity": "sha512-VhEArSKyCC8dv223fltbMOqaJInFZqIqLABLnD3VLhclriF9sxnAJu6ZvnIMI+p7+ByZBxXd4otTrLAeeMTImg==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -6085,7 +6088,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "ws": "~8.11.0" }, "engines": { "node": ">=10.0.0" @@ -6111,6 +6114,27 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -11043,27 +11067,51 @@ } }, "node_modules/socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "version": "4.6.0-alpha1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.0-alpha1.tgz", + "integrity": "sha512-axR8hRCykqQwNZ5JUDerKqzFHP0g0hOtSfmFd3TLn/frVO5BBBo95Gb7drYD9RtA8TUcK37fPCfm7WCMm8ZVyA==", "dev": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" + "engine.io": "~6.3.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==", - "dev": true + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, "node_modules/socket.io-msgpack-parser": { "version": "3.0.1", @@ -14620,10 +14668,13 @@ "dev": true }, "@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/diff": { "version": "5.0.2", @@ -17760,9 +17811,9 @@ } }, "engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.3.1.tgz", + "integrity": "sha512-VhEArSKyCC8dv223fltbMOqaJInFZqIqLABLnD3VLhclriF9sxnAJu6ZvnIMI+p7+ByZBxXd4otTrLAeeMTImg==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -17774,7 +17825,16 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "ws": "~8.11.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + } } }, "engine.io-client": { @@ -21512,24 +21572,36 @@ } }, "socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "version": "4.6.0-alpha1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.0-alpha1.tgz", + "integrity": "sha512-axR8hRCykqQwNZ5JUDerKqzFHP0g0hOtSfmFd3TLn/frVO5BBBo95Gb7drYD9RtA8TUcK37fPCfm7WCMm8ZVyA==", "dev": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" + "engine.io": "~6.3.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" } }, "socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==", - "dev": true + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "requires": { + "ws": "~8.11.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + } + } }, "socket.io-msgpack-parser": { "version": "3.0.1", diff --git a/package.json b/package.json index 0e6d29d6..bd053b2c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/connection-state-recovery.ts b/test/connection-state-recovery.ts new file mode 100644 index 00000000..8aa9ee41 --- /dev/null +++ b/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(); + }); + }); + }); + }); +}); diff --git a/test/index.ts b/test/index.ts index a3f6f8d4..69d0d8d9 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2,3 +2,4 @@ import "./url"; import "./connection"; import "./socket"; import "./node"; +import "./connection-state-recovery"; diff --git a/test/support/server.ts b/test/support/server.ts index 42d4e52d..14b2cfa2 100644 --- a/test/support/server.ts +++ b/test/support/server.ts @@ -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) => {