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 2f6efdf..f2f85ed 100644 --- a/src/socket-io.service.ts +++ b/src/socket-io.service.ts @@ -1,16 +1,66 @@ 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 { - subscribersCounter: Record = {}; - eventObservables$: Record> = {}; - namespaces: Record = {}; - ioSocket: any; - emptyConfig: SocketIoConfig = { +export class WrappedSocket implements WrappedSocketIface { + private readonly subscribersCounter: Record = {}; + private readonly eventObservables$: Record> = {}; + private readonly namespaces: Record = {}; + readonly ioSocket: IoSocket; + private readonly 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,36 +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 { - 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 + off( + eventName?: Ev, + listener?: EventListener + ): this { this.ioSocket.off(eventName, listener); return this; } @@ -203,23 +287,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; }