From e5227e2e4c0a9f61340df7b3cf5197585f710aed Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Tue, 11 Jul 2023 22:14:59 +0400 Subject: [PATCH] feat(socketio): allow injecting the disconnection reason (#2373) closes #2372 --- .../snippets/socketio/socket-service.ts | 4 +- .../third-parties/socketio/jest.config.js | 6 +- packages/third-parties/socketio/readme.md | 4 +- .../src/class/SocketHandlersBuilder.spec.ts | 61 +++++++++++++++++++ .../src/class/SocketHandlersBuilder.ts | 7 ++- .../socketio/src/decorators/reason.spec.ts | 25 ++++++++ .../socketio/src/decorators/reason.ts | 30 +++++++++ packages/third-parties/socketio/src/index.ts | 1 + .../socketio/src/interfaces/SocketFilters.ts | 3 +- .../src/services/SocketIOService.spec.ts | 36 +++-------- .../socketio/src/services/SocketIOService.ts | 4 +- 11 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 packages/third-parties/socketio/src/decorators/reason.spec.ts create mode 100644 packages/third-parties/socketio/src/decorators/reason.ts diff --git a/docs/tutorials/snippets/socketio/socket-service.ts b/docs/tutorials/snippets/socketio/socket-service.ts index 82555eb2071..9650cb69c4c 100644 --- a/docs/tutorials/snippets/socketio/socket-service.ts +++ b/docs/tutorials/snippets/socketio/socket-service.ts @@ -1,4 +1,4 @@ -import {IO, Nsp, Socket, SocketService, SocketSession} from "@tsed/socketio"; +import {IO, Nsp, Socket, SocketService, SocketSession, Reason} from "@tsed/socketio"; import * as SocketIO from "socket.io"; @SocketService("/my-namespace") @@ -23,5 +23,5 @@ export class MySocketService { /** * Triggered when a client disconnects from the Namespace. */ - $onDisconnect(@Socket socket: SocketIO.Socket) {} + $onDisconnect(@Socket socket: SocketIO.Socket, @Reason reason: string) {} } diff --git a/packages/third-parties/socketio/jest.config.js b/packages/third-parties/socketio/jest.config.js index 8d52d931675..e0a6005ae09 100644 --- a/packages/third-parties/socketio/jest.config.js +++ b/packages/third-parties/socketio/jest.config.js @@ -8,10 +8,10 @@ module.exports = { }, coverageThreshold: { global: { - statements: 99.59, - branches: 94.5, + statements: 99.6, + branches: 94.59, functions: 100, - lines: 99.59 + lines: 99.6 } } }; diff --git a/packages/third-parties/socketio/readme.md b/packages/third-parties/socketio/readme.md index af4c1c23b40..eb96bd44f61 100644 --- a/packages/third-parties/socketio/readme.md +++ b/packages/third-parties/socketio/readme.md @@ -67,7 +67,7 @@ Example: ```typescript import * as SocketIO from "socket.io"; -import {SocketService, IO, Nsp, Socket, SocketSession} from "@tsed/socketio"; +import {SocketService, IO, Nsp, Socket, SocketSession, Reason} from "@tsed/socketio"; @SocketService("/my-namespace") export class MySocketService { @@ -88,7 +88,7 @@ export class MySocketService { /** * Triggered when a client disconnects from the Namespace. */ - $onDisconnect(@Socket socket: SocketIO.Socket) {} + $onDisconnect(@Socket socket: SocketIO.Socket, @Reason reason: string) {} } ``` diff --git a/packages/third-parties/socketio/src/class/SocketHandlersBuilder.spec.ts b/packages/third-parties/socketio/src/class/SocketHandlersBuilder.spec.ts index 54ffa0e6080..d438a79033e 100644 --- a/packages/third-parties/socketio/src/class/SocketHandlersBuilder.spec.ts +++ b/packages/third-parties/socketio/src/class/SocketHandlersBuilder.spec.ts @@ -197,6 +197,50 @@ describe("SocketHandlersBuilder", () => { } ); }); + + it("should pass the disconnection reason", () => { + const instance = { + $onDisconnect: jest.fn() + }; + + const provider: any = { + store: { + get: jest.fn().mockReturnValue({ + injectNamespace: "nsp", + handlers: { + $onDisconnect: { + eventName: "onDisconnect" + } + } + }) + } + }; + const nspStub: any = {nsp: "nsp"}; + const reason = "transport error"; + const socketStub: any = { + on: jest.fn() + }; + + const builder: any = new SocketHandlersBuilder(provider, { + get() { + return instance; + } + } as any); + const invokeStub = jest.spyOn(builder, "invoke").mockReturnValue(undefined); + jest.spyOn(builder, "destroySession").mockReturnValue(undefined); + + builder.onDisconnect(socketStub, nspStub, reason); + + expect(invokeStub).toBeCalledWith( + instance, + {eventName: "onDisconnect"}, + { + reason, + socket: socketStub, + nsp: nspStub + } + ); + }); }); describe("createSession()", () => { it("should create session for the socket", () => { @@ -390,6 +434,23 @@ describe("SocketHandlersBuilder", () => { }); }); + describe("when REASON", () => { + it("should return a disconnect reason", () => { + const {builder} = createFixture(); + + const result = builder.buildParameters( + { + 0: { + filter: SocketFilters.REASON + } + }, + {reason: "transport error"} + ); + + expect(result).toEqual(["transport error"]); + }); + }); + describe("when ERROR", () => { it("should return a list of parameters", () => { const {builder} = createFixture(); diff --git a/packages/third-parties/socketio/src/class/SocketHandlersBuilder.ts b/packages/third-parties/socketio/src/class/SocketHandlersBuilder.ts index 081d89f453a..e9caa1c8cac 100644 --- a/packages/third-parties/socketio/src/class/SocketHandlersBuilder.ts +++ b/packages/third-parties/socketio/src/class/SocketHandlersBuilder.ts @@ -92,12 +92,12 @@ export class SocketHandlersBuilder { } } - public onDisconnect(socket: Socket, nsp: Namespace) { + public onDisconnect(socket: Socket, nsp: Namespace, reason?: string) { const instance = this.injector.get(this.provider.token); const {socketProviderMetadata} = this; if (instance.$onDisconnect) { - this.invoke(instance, socketProviderMetadata.$onDisconnect, {socket, nsp}); + this.invoke(instance, socketProviderMetadata.$onDisconnect, {socket, nsp, reason}); } this.destroySession(socket); @@ -253,6 +253,9 @@ export class SocketHandlersBuilder { case SocketFilters.SOCKET_NSP: return scope.socket.nsp; + + case SocketFilters.REASON: + return scope.reason; } }); } diff --git a/packages/third-parties/socketio/src/decorators/reason.spec.ts b/packages/third-parties/socketio/src/decorators/reason.spec.ts new file mode 100644 index 00000000000..b2efdd21c3c --- /dev/null +++ b/packages/third-parties/socketio/src/decorators/reason.spec.ts @@ -0,0 +1,25 @@ +import {Store} from "@tsed/core"; +import {Nsp, SocketErr} from "../index"; +import {Reason} from "./reason"; + +describe("Reason", () => { + it("should set metadata", () => { + class Test {} + + Reason(Test, "test", 0); + const store = Store.from(Test); + + expect(store.get("socketIO")).toEqual({ + handlers: { + test: { + parameters: { + "0": { + filter: "reason", + mapIndex: undefined + } + } + } + } + }); + }); +}); diff --git a/packages/third-parties/socketio/src/decorators/reason.ts b/packages/third-parties/socketio/src/decorators/reason.ts new file mode 100644 index 00000000000..f864e6edc4e --- /dev/null +++ b/packages/third-parties/socketio/src/decorators/reason.ts @@ -0,0 +1,30 @@ +import {SocketFilter} from "./socketFilter"; +import {SocketFilters} from "../interfaces/SocketFilters"; + +/** + * Inject the disconnection reason into the decorated parameter. + * + * This decorator is used in conjunction with the `$onDisconnect` event handler to handle disconnection reasons in SocketIO services. + * It allows you to access the reason for the disconnection in your method implementation. For details please refer to the [Socket.io documentation](https://socket.io/docs/v4/server-api/#event-disconnect). + * + * @example + * ```typescript + * @SocketService("/nsp") + * export class MyWS { + * public async $onDisconnect( + * @Reason reason: string = '' + * ) { + * // your implementation + * } + * } + * ``` + * + * @experimental This decorator is experimental and may change or be removed in future versions. + * @param target + * @param {string} propertyKey + * @param {number} index + * @decorator + */ +export function Reason(target: unknown, propertyKey: string, index: number) { + return SocketFilter(SocketFilters.REASON)(target, propertyKey, index); +} diff --git a/packages/third-parties/socketio/src/index.ts b/packages/third-parties/socketio/src/index.ts index 9358add148b..aed327f1519 100644 --- a/packages/third-parties/socketio/src/index.ts +++ b/packages/third-parties/socketio/src/index.ts @@ -16,6 +16,7 @@ export * from "./decorators/inputAndBroadcastOthers"; export * from "./decorators/inputAndEmit"; export * from "./decorators/io"; export * from "./decorators/nsp"; +export * from "./decorators/reason"; export * from "./decorators/socket"; export * from "./decorators/socketErr"; export * from "./decorators/socketEventName"; diff --git a/packages/third-parties/socketio/src/interfaces/SocketFilters.ts b/packages/third-parties/socketio/src/interfaces/SocketFilters.ts index 8adbd0a10f0..9a8a017acc4 100644 --- a/packages/third-parties/socketio/src/interfaces/SocketFilters.ts +++ b/packages/third-parties/socketio/src/interfaces/SocketFilters.ts @@ -8,5 +8,6 @@ export enum SocketFilters { NSP = "nsp", SESSION = "session", ERR = "error", - SOCKET_NSP = "socket_nsp" + SOCKET_NSP = "socket_nsp", + REASON = "reason" } diff --git a/packages/third-parties/socketio/src/services/SocketIOService.spec.ts b/packages/third-parties/socketio/src/services/SocketIOService.spec.ts index 2f70f648544..a810ac04e3b 100644 --- a/packages/third-parties/socketio/src/services/SocketIOService.spec.ts +++ b/packages/third-parties/socketio/src/services/SocketIOService.spec.ts @@ -24,48 +24,30 @@ async function createServiceFixture() { use: ioStub } ]); + const reason = "transport error"; - return {namespace, ioStub, service, instance, socket}; + return {namespace, ioStub, service, instance, socket, reason}; } describe("SocketIOService", () => { beforeEach(() => PlatformTest.create()); afterEach(() => PlatformTest.reset()); - describe("getNsp(string)", () => { - it("should call io.of and create namespace", async () => { - const {service, namespace, ioStub, socket, instance} = await createServiceFixture(); + describe("getNsp", () => { + it.each([{input: "/"}, {input: /test/}])("should call io.of with $input and create namespace", async ({input}) => { + const {service, namespace, ioStub, socket, instance, reason} = await createServiceFixture(); - const nspConf = service.getNsp("/"); + const nspConf = service.getNsp(input); nspConf.instances.push(instance); namespace.on.mock.calls[0][1](socket); - socket.on.mock.calls[0][1](); + socket.on.mock.calls[0][1](reason); - expect(ioStub.of).toBeCalledWith("/"); + expect(ioStub.of).toBeCalledWith(input); expect(namespace.on).toBeCalledWith("connection", expect.any(Function)); expect(instance.onConnection).toBeCalledWith(socket, namespace); expect(socket.on).toBeCalledWith("disconnect", expect.any(Function)); - expect(instance.onDisconnect).toBeCalledWith(socket, namespace); - }); - }); - - describe("getNsp(RegExp)", () => { - it("should call io.of and create namespace", async () => { - const {service, namespace, ioStub, socket, instance} = await createServiceFixture(); - const regexp = new RegExp(/test/); - - const nspConf = service.getNsp(regexp); - nspConf.instances.push(instance); - - namespace.on.mock.calls[0][1](socket); - socket.on.mock.calls[0][1](); - - expect(ioStub.of).toBeCalledWith(regexp); - expect(namespace.on).toBeCalledWith("connection", expect.any(Function)); - expect(instance.onConnection).toBeCalledWith(socket, namespace); - expect(socket.on).toBeCalledWith("disconnect", expect.any(Function)); - expect(instance.onDisconnect).toBeCalledWith(socket, namespace); + expect(instance.onDisconnect).toBeCalledWith(socket, namespace, reason); }); }); }); diff --git a/packages/third-parties/socketio/src/services/SocketIOService.ts b/packages/third-parties/socketio/src/services/SocketIOService.ts index 0c7734d0f73..afefb81925a 100644 --- a/packages/third-parties/socketio/src/services/SocketIOService.ts +++ b/packages/third-parties/socketio/src/services/SocketIOService.ts @@ -33,9 +33,9 @@ export class SocketIOService { builder.onConnection(socket, conf.nsp); }); - socket.on("disconnect", () => { + socket.on("disconnect", (reason: string) => { conf.instances.forEach((builder: SocketHandlersBuilder) => { - builder.onDisconnect(socket, conf.nsp); + builder.onDisconnect(socket, conf.nsp, reason); }); }); });