Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix typing #191

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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