Skip to content

Commit

Permalink
Fix and lock typescript typings
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
barbieri committed Jan 2, 2025
1 parent 710692e commit e17eb45
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 44 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
141 changes: 119 additions & 22 deletions src/socket-io.service.ts
Original file line number Diff line number Diff line change
@@ -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<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 {
export class WrappedSocket implements WrappedSocketIface<WrappedSocket> {
subscribersCounter: Record<string, number> = {};
eventObservables$: Record<string, Observable<any>> = {};
namespaces: Record<string, WrappedSocket> = {};
ioSocket: any;
ioSocket: IoSocket;
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,23 +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 {
off<Ev extends EventNames>(
eventName?: Ev,
listener?: EventListener<Ev>
): this {
if (!eventName) {
// Remove all listeners for all events
this.ioSocket.offAny();
Expand Down Expand Up @@ -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;
}

Expand Down

0 comments on commit e17eb45

Please sign in to comment.