From 710692e4ba95e8143bc08431fe70dccd2ac091b9 Mon Sep 17 00:00:00 2001 From: Gustavo Sverzut Barbieri Date: Thu, 2 Jan 2025 12:34:50 -0300 Subject: [PATCH 1/4] Fix: off() takes a single function, not array --- src/socket-io.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket-io.service.ts b/src/socket-io.service.ts index 2f6efdf..f65a46b 100644 --- a/src/socket-io.service.ts +++ b/src/socket-io.service.ts @@ -141,7 +141,7 @@ export class WrappedSocket { return this.ioSocket.listenersAnyOutgoing(); } - off(eventName?: string, listener?: Function[]): this { + off(eventName?: string, listener?: Function): this { if (!eventName) { // Remove all listeners for all events this.ioSocket.offAny(); From e17eb45556e6b70999842a27cf6d2e797edff5b9 Mon Sep 17 00:00:00 2001 From: Gustavo Sverzut Barbieri Date: Thu, 2 Jan 2025 13:53:07 -0300 Subject: [PATCH 2/4] Fix and lock typescript typings The previous attempt to manually sync missed some details, then remove the outdated @types/socket-io and use the built-in types instead. Then do a mapping of the original IoSocket type to the wrapped, forcing the wrapper to be returned for chained, properties should alway match. This will force the wrapper to be in sync without much effort. --- package-lock.json | 21 +----- package.json | 2 - src/socket-io.service.ts | 141 +++++++++++++++++++++++++++++++++------ 3 files changed, 120 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 274cd9d..662543f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "ngx-socket-io", - "version": "4.8.0", + "version": "4.8.2", "license": "MIT", "dependencies": { "core-js": "^3.39.0", @@ -20,8 +20,6 @@ "@angular/compiler-cli": "^19.0.0", "@angular/core": "^19.0.0", "@types/node": "^22.9.1", - "@types/socket.io": "^3.0.1", - "@types/socket.io-client": "^1.4.36", "husky": "^9.1.7", "ng-packagr": "^19.0.0", "prettier": "^3.3.3", @@ -1856,23 +1854,6 @@ "undici-types": "~6.19.8" } }, - "node_modules/@types/socket.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", - "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "socket.io": "*" - } - }, - "node_modules/@types/socket.io-client": { - "version": "1.4.36", - "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", - "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", - "dev": true, - "license": "MIT" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", diff --git a/package.json b/package.json index c34196e..5246044 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,6 @@ "@angular/compiler-cli": "^19.0.0", "@angular/core": "^19.0.0", "@types/node": "^22.9.1", - "@types/socket.io": "^3.0.1", - "@types/socket.io-client": "^1.4.36", "husky": "^9.1.7", "ng-packagr": "^19.0.0", "prettier": "^3.3.3", diff --git a/src/socket-io.service.ts b/src/socket-io.service.ts index f65a46b..23834e8 100644 --- a/src/socket-io.service.ts +++ b/src/socket-io.service.ts @@ -1,15 +1,65 @@ import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; -import * as io from 'socket.io-client'; +import * as ioModule from 'socket.io-client'; +import type { io, Socket } from 'socket.io-client'; +import type { + ReservedOrUserListener, + ReservedOrUserEventNames, + DefaultEventsMap, +} from '@socket.io/component-emitter'; + +export type IoSocket = Socket; +// This is not exported in the original, but let's export as helpers for those declaring disconnect handlers +export type DisconnectDescription = + | Error + | { + description: string; + context?: unknown; + }; + +// Not exported but needed to properly map ReservedEvents to their signatures +interface SocketReservedEvents { + connect: () => void; + connect_error: (err: Error) => void; + disconnect: ( + reason: Socket.DisconnectReason, + description?: DisconnectDescription + ) => void; +} + +type EventNames = ReservedOrUserEventNames< + SocketReservedEvents, + DefaultEventsMap +>; +type EventListener = ReservedOrUserListener< + SocketReservedEvents, + DefaultEventsMap, + Ev +>; +type EventParameters = Parameters>; +type EventPayload = + EventParameters extends [] ? undefined : EventParameters[0]; + +type IgnoredWrapperEvents = 'receiveBuffer' | 'sendBuffer'; + +type WrappedSocketIface = { + [K in Exclude]: IoSocket[K] extends ( + ...args: any[] + ) => IoSocket + ? (...args: Parameters) => Wrapper // chainable methods on().off().emit()... + : IoSocket[K] extends IoSocket + ? Wrapper // ie: volatile is a getter + : IoSocket[K]; +}; import { SocketIoConfig } from './config/socket-io.config'; -export class WrappedSocket { +export class WrappedSocket implements WrappedSocketIface { subscribersCounter: Record = {}; eventObservables$: Record> = {}; namespaces: Record = {}; - ioSocket: any; + ioSocket: IoSocket; emptyConfig: SocketIoConfig = { url: '', options: {}, @@ -21,10 +71,35 @@ export class WrappedSocket { } const url: string = config.url; const options: any = config.options; - const ioFunc = (io as any).default ? (io as any).default : io; + const ioFunc = ( + (ioModule as any).default ? (ioModule as any).default : ioModule + ) as typeof io; this.ioSocket = ioFunc(url, options); } + get auth(): Socket['auth'] { + return this.ioSocket.auth; + } + + set auth(value: Socket['auth']) { + this.ioSocket.auth = value; + } + + /** readonly access to io manager */ + get io(): Socket['io'] { + return this.ioSocket.io; + } + + /** alias to connect */ + get open(): WrappedSocket['connect'] { + return this.connect; + } + + /** alias to disconnect */ + get close(): WrappedSocket['disconnect'] { + return this.disconnect; + } + /** * Gets a WrappedSocket for the given namespace. * @@ -57,12 +132,15 @@ export class WrappedSocket { return created; } - on(eventName: string, callback: Function): this { + on(eventName: Ev, callback: EventListener): this { this.ioSocket.on(eventName, callback); return this; } - once(eventName: string, callback: Function): this { + once( + eventName: Ev, + callback: EventListener + ): this { this.ioSocket.once(eventName, callback); return this; } @@ -91,17 +169,22 @@ export class WrappedSocket { return this.ioSocket.emitWithAck.apply(this.ioSocket, arguments); } - removeListener(_eventName: string, _callback?: Function): this { + removeListener( + _eventName?: Ev, + _callback?: EventListener + ): this { this.ioSocket.removeListener.apply(this.ioSocket, arguments); return this; } - removeAllListeners(_eventName?: string): this { + removeAllListeners(_eventName?: Ev): this { this.ioSocket.removeAllListeners.apply(this.ioSocket, arguments); return this; } - fromEvent(eventName: string): Observable { + fromEvent, Ev extends EventNames>( + eventName: Ev + ): Observable { if (!this.subscribersCounter[eventName]) { this.subscribersCounter[eventName] = 0; } @@ -112,11 +195,14 @@ export class WrappedSocket { const listener = (data: T) => { observer.next(data); }; - this.ioSocket.on(eventName, listener); + this.ioSocket.on(eventName, listener as EventListener); return () => { this.subscribersCounter[eventName]--; if (this.subscribersCounter[eventName] === 0) { - this.ioSocket.removeListener(eventName, listener); + this.ioSocket.removeListener( + eventName, + listener as EventListener + ); delete this.eventObservables$[eventName]; } }; @@ -125,23 +211,34 @@ export class WrappedSocket { return this.eventObservables$[eventName]; } - fromOneTimeEvent(eventName: string): Promise { - return new Promise(resolve => this.once(eventName, resolve)); + fromOneTimeEvent, Ev extends EventNames>( + eventName: Ev + ): Promise { + return new Promise(resolve => + this.once(eventName, resolve as EventListener) + ); } - listeners(eventName: string): Function[] { + listeners(eventName: Ev): EventListener[] { return this.ioSocket.listeners(eventName); } - listenersAny(): Function[] { + hasListeners(eventName: Ev): boolean { + return this.ioSocket.hasListeners(eventName); + } + + listenersAny(): ((...args: any[]) => void)[] { return this.ioSocket.listenersAny(); } - listenersAnyOutgoing(): Function[] { + listenersAnyOutgoing(): ((...args: any[]) => void)[] { return this.ioSocket.listenersAnyOutgoing(); } - off(eventName?: string, listener?: Function): this { + off( + eventName?: Ev, + listener?: EventListener + ): this { if (!eventName) { // Remove all listeners for all events this.ioSocket.offAny(); @@ -203,23 +300,23 @@ export class WrappedSocket { return this; } - get active(): boolean { + get active(): Socket['active'] { return this.ioSocket.active; } - get connected(): boolean { + get connected(): Socket['connected'] { return this.ioSocket.connected; } - get disconnected(): boolean { + get disconnected(): Socket['disconnected'] { return this.ioSocket.disconnected; } - get recovered(): boolean { + get recovered(): Socket['recovered'] { return this.ioSocket.recovered; } - get id(): string { + get id(): Socket['id'] { return this.ioSocket.id; } From cf66bfbc24d964b0842582665151101e42fb479b Mon Sep 17 00:00:00 2001 From: Gustavo Sverzut Barbieri Date: Thu, 2 Jan 2025 14:00:33 -0300 Subject: [PATCH 3/4] Breaking: mark internal fields as private, all as readonly None of these fields should be reassigned once they are created, so they are all readonly. Except by ioSocket, all are of internal use, so keep them private. --- src/socket-io.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/socket-io.service.ts b/src/socket-io.service.ts index 23834e8..07b8342 100644 --- a/src/socket-io.service.ts +++ b/src/socket-io.service.ts @@ -56,11 +56,11 @@ type WrappedSocketIface = { import { SocketIoConfig } from './config/socket-io.config'; export class WrappedSocket implements WrappedSocketIface { - subscribersCounter: Record = {}; - eventObservables$: Record> = {}; - namespaces: Record = {}; - ioSocket: IoSocket; - emptyConfig: SocketIoConfig = { + private readonly subscribersCounter: Record = {}; + private readonly eventObservables$: Record> = {}; + private readonly namespaces: Record = {}; + readonly ioSocket: IoSocket; + private readonly emptyConfig: SocketIoConfig = { url: '', options: {}, }; From 91610011862cd7bc5e2d3a504a657ea8a981f21b Mon Sep 17 00:00:00 2001 From: Gustavo Sverzut Barbieri Date: Thu, 2 Jan 2025 14:07:43 -0300 Subject: [PATCH 4/4] Breaking: fix off() behavior, sync with socket-io-client off() without an event name just unregisters all event handlers via EventEmitter. The comment implied offAny() would do this, but it's not the case: that just removes the onAny() handlers. If we really wanted to remove all event handlers for everything we should be calling: - offAny() - offAnyOutgoing() - off() But that would not match the wrapped behavior, so let's just keep it simple. --- src/socket-io.service.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/socket-io.service.ts b/src/socket-io.service.ts index 07b8342..f2f85ed 100644 --- a/src/socket-io.service.ts +++ b/src/socket-io.service.ts @@ -239,19 +239,6 @@ export class WrappedSocket implements WrappedSocketIface { eventName?: Ev, listener?: EventListener ): this { - if (!eventName) { - // Remove all listeners for all events - this.ioSocket.offAny(); - return this; - } - - if (eventName && !listener) { - // Remove all listeners for that event - this.ioSocket.off(eventName); - return this; - } - - // Removes the specified listener from the listener array for the event named this.ioSocket.off(eventName, listener); return this; }