diff --git a/packages/platform-socket.io/adapters/io-adapter.ts b/packages/platform-socket.io/adapters/io-adapter.ts index ab08fde131a..b997ab15f06 100644 --- a/packages/platform-socket.io/adapters/io-adapter.ts +++ b/packages/platform-socket.io/adapters/io-adapter.ts @@ -83,6 +83,10 @@ export class IoAdapter extends AbstractWsAdapter { return { data: payload }; } + public bindClientDisconnect(client: Socket, callback: Function) { + client.on(DISCONNECT_EVENT, (reason: string) => callback(reason)); + } + public async close(server: Server): Promise { if (this.forceCloseConnections && server.httpServer === this.httpServer) { return; diff --git a/packages/websockets/interfaces/hooks/on-gateway-disconnect.interface.ts b/packages/websockets/interfaces/hooks/on-gateway-disconnect.interface.ts index e86e27d8fa7..728738ae4c5 100644 --- a/packages/websockets/interfaces/hooks/on-gateway-disconnect.interface.ts +++ b/packages/websockets/interfaces/hooks/on-gateway-disconnect.interface.ts @@ -2,5 +2,5 @@ * @publicApi */ export interface OnGatewayDisconnect { - handleDisconnect(client: T): any; + handleDisconnect(client: T, reason?: string): any; } diff --git a/packages/websockets/interfaces/nest-gateway.interface.ts b/packages/websockets/interfaces/nest-gateway.interface.ts index 89472d3626d..b5006c96e4a 100644 --- a/packages/websockets/interfaces/nest-gateway.interface.ts +++ b/packages/websockets/interfaces/nest-gateway.interface.ts @@ -4,5 +4,5 @@ export interface NestGateway { afterInit?: (server: any) => void; handleConnection?: (...args: any[]) => void; - handleDisconnect?: (client: any) => void; + handleDisconnect?: (client: any, reason?: string) => void; } diff --git a/packages/websockets/test/web-sockets-controller.spec.ts b/packages/websockets/test/web-sockets-controller.spec.ts index 576228a9b03..27056c4f1b2 100644 --- a/packages/websockets/test/web-sockets-controller.spec.ts +++ b/packages/websockets/test/web-sockets-controller.spec.ts @@ -412,6 +412,70 @@ describe('WebSocketsController', () => { instance.subscribeDisconnectEvent(gateway, event); expect(subscribe.called).to.be.true; }); + + describe('when handling disconnect events', () => { + let handleDisconnectSpy: sinon.SinonSpy; + + beforeEach(() => { + handleDisconnectSpy = sinon.spy(); + (gateway as any).handleDisconnect = handleDisconnectSpy; + }); + + it('should call handleDisconnect with client and reason when data contains both', () => { + const mockClient = { id: 'test-client' }; + const mockReason = 'client namespace disconnect'; + const disconnectData = { client: mockClient, reason: mockReason }; + + let subscriptionCallback: Function | undefined; + event.subscribe = (callback: Function) => { + subscriptionCallback = callback; + }; + + instance.subscribeDisconnectEvent(gateway, event); + + if (subscriptionCallback) { + subscriptionCallback(disconnectData); + } + + expect(handleDisconnectSpy.calledOnce).to.be.true; + expect(handleDisconnectSpy.calledWith(mockClient, mockReason)).to.be + .true; + }); + + it('should call handleDisconnect with only client for backward compatibility', () => { + const mockClient = { id: 'test-client' }; + + let subscriptionCallback: Function | undefined; + event.subscribe = (callback: Function) => { + subscriptionCallback = callback; + }; + + instance.subscribeDisconnectEvent(gateway, event); + + if (subscriptionCallback) { + subscriptionCallback(mockClient); + } + + expect(handleDisconnectSpy.calledOnce).to.be.true; + expect(handleDisconnectSpy.calledWith(mockClient)).to.be.true; + }); + + it('should handle null/undefined data gracefully', () => { + let subscriptionCallback: Function | undefined; + event.subscribe = (callback: Function) => { + subscriptionCallback = callback; + }; + + instance.subscribeDisconnectEvent(gateway, event); + + if (subscriptionCallback) { + subscriptionCallback(null); + } + + expect(handleDisconnectSpy.calledOnce).to.be.true; + expect(handleDisconnectSpy.calledWith(null)).to.be.true; + }); + }); }); describe('subscribeMessages', () => { const gateway = new Test(); diff --git a/packages/websockets/web-sockets-controller.ts b/packages/websockets/web-sockets-controller.ts index bcacf245f2a..714bb3356f2 100644 --- a/packages/websockets/web-sockets-controller.ts +++ b/packages/websockets/web-sockets-controller.ts @@ -140,7 +140,9 @@ export class WebSocketsController { const disconnectHook = adapter.bindClientDisconnect; disconnectHook && - disconnectHook.call(adapter, client, () => disconnect.next(client)); + disconnectHook.call(adapter, client, (reason?: string) => + disconnect.next({ client, reason }), + ); }; } @@ -163,8 +165,21 @@ export class WebSocketsController { public subscribeDisconnectEvent(instance: NestGateway, event: Subject) { if (instance.handleDisconnect) { event - .pipe(distinctUntilChanged()) - .subscribe(instance.handleDisconnect.bind(instance)); + .pipe( + distinctUntilChanged((prev, curr) => { + const prevClient = prev?.client || prev; + const currClient = curr?.client || curr; + return prevClient === currClient; + }), + ) + .subscribe((data: any) => { + if (data && typeof data === 'object' && 'client' in data) { + instance.handleDisconnect!(data.client, data.reason); + } else { + // Backward compatibility: if it's just the client + instance.handleDisconnect!(data); + } + }); } }