Skip to content

Commit

Permalink
Fix typing (#191)
Browse files Browse the repository at this point in the history
* Fix: off() takes a single function, not array

* 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.

* 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.

* 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.
  • Loading branch information
barbieri authored Feb 8, 2025
1 parent d5f9d72 commit 04f35ac
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 61 deletions.
21 changes: 1 addition & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
162 changes: 123 additions & 39 deletions src/socket-io.service.ts
Original file line number Diff line number Diff line change
@@ -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<Ev extends EventNames> = ReservedOrUserListener<
SocketReservedEvents,
DefaultEventsMap,
Ev
>;
type EventParameters<Ev extends EventNames> = Parameters<EventListener<Ev>>;
type EventPayload<Ev extends EventNames> =
EventParameters<Ev> extends [] ? undefined : EventParameters<Ev>[0];

type IgnoredWrapperEvents = 'receiveBuffer' | 'sendBuffer';

type WrappedSocketIface<Wrapper> = {
[K in Exclude<keyof IoSocket, IgnoredWrapperEvents>]: IoSocket[K] extends (
...args: any[]
) => IoSocket
? (...args: Parameters<IoSocket[K]>) => 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<string, number> = {};
eventObservables$: Record<string, Observable<any>> = {};
namespaces: Record<string, WrappedSocket> = {};
ioSocket: any;
emptyConfig: SocketIoConfig = {
export class WrappedSocket implements WrappedSocketIface<WrappedSocket> {
private readonly subscribersCounter: Record<string, number> = {};
private readonly eventObservables$: Record<string, Observable<any>> = {};
private readonly namespaces: Record<string, WrappedSocket> = {};
readonly ioSocket: IoSocket;
private readonly emptyConfig: SocketIoConfig = {
url: '',
options: {},
};
Expand All @@ -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.
*
Expand Down Expand Up @@ -57,12 +132,15 @@ export class WrappedSocket {
return created;
}

on(eventName: string, callback: Function): this {
on<Ev extends EventNames>(eventName: Ev, callback: EventListener<Ev>): this {
this.ioSocket.on(eventName, callback);
return this;
}

once(eventName: string, callback: Function): this {
once<Ev extends EventNames>(
eventName: Ev,
callback: EventListener<Ev>
): this {
this.ioSocket.once(eventName, callback);
return this;
}
Expand Down Expand Up @@ -91,17 +169,22 @@ export class WrappedSocket {
return this.ioSocket.emitWithAck.apply(this.ioSocket, arguments);
}

removeListener(_eventName: string, _callback?: Function): this {
removeListener<Ev extends EventNames>(
_eventName?: Ev,
_callback?: EventListener<Ev>
): this {
this.ioSocket.removeListener.apply(this.ioSocket, arguments);
return this;
}

removeAllListeners(_eventName?: string): this {
removeAllListeners<Ev extends EventNames>(_eventName?: Ev): this {
this.ioSocket.removeAllListeners.apply(this.ioSocket, arguments);
return this;
}

fromEvent<T>(eventName: string): Observable<T> {
fromEvent<T extends EventPayload<Ev>, Ev extends EventNames>(
eventName: Ev
): Observable<T> {
if (!this.subscribersCounter[eventName]) {
this.subscribersCounter[eventName] = 0;
}
Expand All @@ -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<Ev>);
return () => {
this.subscribersCounter[eventName]--;
if (this.subscribersCounter[eventName] === 0) {
this.ioSocket.removeListener(eventName, listener);
this.ioSocket.removeListener(
eventName,
listener as EventListener<Ev>
);
delete this.eventObservables$[eventName];
}
};
Expand All @@ -125,36 +211,34 @@ export class WrappedSocket {
return this.eventObservables$[eventName];
}

fromOneTimeEvent<T>(eventName: string): Promise<T> {
return new Promise<T>(resolve => this.once(eventName, resolve));
fromOneTimeEvent<T extends EventPayload<Ev>, Ev extends EventNames>(
eventName: Ev
): Promise<T> {
return new Promise<T>(resolve =>
this.once(eventName, resolve as EventListener<Ev>)
);
}

listeners(eventName: string): Function[] {
listeners<Ev extends EventNames>(eventName: Ev): EventListener<Ev>[] {
return this.ioSocket.listeners(eventName);
}

listenersAny(): Function[] {
hasListeners<Ev extends EventNames>(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<Ev extends EventNames>(
eventName?: Ev,
listener?: EventListener<Ev>
): this {
this.ioSocket.off(eventName, listener);
return this;
}
Expand Down Expand Up @@ -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;
}

Expand Down

0 comments on commit 04f35ac

Please sign in to comment.