diff --git a/README.md b/README.md index f0a4d7b..e42f813 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,10 @@ The only addition is the `fromEvent` method, which returns an `Observable` that ### `socket.of(namespace: string)` -Takes an namespace. -Works the same as in Socket.IO. +Takes a namespace and returns an instance based on the current config and the given namespace, +that is added to the end of the current url. +See [Namespaces - Client Initialization](https://socket.io/docs/v4/namespaces/#client-initialization). +Instances are reused based on the namespace. ### `socket.on(eventName: string, callback: Function)` diff --git a/src/socket-io.service.ts b/src/socket-io.service.ts index 24b1e0d..2f6efdf 100644 --- a/src/socket-io.service.ts +++ b/src/socket-io.service.ts @@ -8,6 +8,7 @@ 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 = { url: '', @@ -24,36 +25,80 @@ export class WrappedSocket { this.ioSocket = ioFunc(url, options); } - of(namespace: string) { - this.ioSocket.of(namespace); - } - - on(eventName: string, callback: Function) { + /** + * Gets a WrappedSocket for the given namespace. + * + * @note if an existing socket exists for the given namespace, it will be reused. + * + * @param namespace the namespace to create a new socket based on the current config. + * If empty or `/`, then the current instance is returned. + * @returns a socket that is bound to the given namespace. If namespace is empty or `/`, + * then `this` is returned, otherwise another instance is returned, creating + * it if it's the first use of such namespace. + */ + of(namespace: string): WrappedSocket { + if (!namespace || namespace === '/') { + return this; + } + const existing = this.namespaces[namespace]; + if (existing) { + return existing; + } + const { url, ...rest } = this.config; + const config = { + url: + !url.endsWith('/') && !namespace.startsWith('/') + ? `${url}/${namespace}` + : `${url}${namespace}`, + ...rest, + }; + const created = new WrappedSocket(config); + this.namespaces[namespace] = created; + return created; + } + + on(eventName: string, callback: Function): this { this.ioSocket.on(eventName, callback); + return this; } - once(eventName: string, callback: Function) { + once(eventName: string, callback: Function): this { this.ioSocket.once(eventName, callback); + return this; + } + + connect(): this { + this.ioSocket.connect(); + return this; } - connect(callback?: (err: any) => void) { - return this.ioSocket.connect(callback); + disconnect(): this { + this.ioSocket.disconnect(); + return this; } - disconnect(_close?: any) { - return this.ioSocket.disconnect.apply(this.ioSocket, arguments); + emit(_eventName: string, ..._args: any[]): this { + this.ioSocket.emit.apply(this.ioSocket, arguments); + return this; } - emit(_eventName: string, ..._args: any[]) { - return this.ioSocket.emit.apply(this.ioSocket, arguments); + send(..._args: any[]): this { + this.ioSocket.send.apply(this.ioSocket, arguments); + return this; } - removeListener(_eventName: string, _callback?: Function) { - return this.ioSocket.removeListener.apply(this.ioSocket, arguments); + emitWithAck<T>(_eventName: string, ..._args: any[]): Promise<T> { + return this.ioSocket.emitWithAck.apply(this.ioSocket, arguments); } - removeAllListeners(_eventName?: string) { - return this.ioSocket.removeAllListeners.apply(this.ioSocket, arguments); + removeListener(_eventName: string, _callback?: Function): this { + this.ioSocket.removeListener.apply(this.ioSocket, arguments); + return this; + } + + removeAllListeners(_eventName?: string): this { + this.ioSocket.removeAllListeners.apply(this.ioSocket, arguments); + return this; } fromEvent<T>(eventName: string): Observable<T> { @@ -84,56 +129,102 @@ export class WrappedSocket { return new Promise<T>(resolve => this.once(eventName, resolve)); } - listeners(eventName: string) { + listeners(eventName: string): Function[] { return this.ioSocket.listeners(eventName); } - listenersAny() { + listenersAny(): Function[] { return this.ioSocket.listenersAny(); } - listenersAnyOutgoing() { + listenersAnyOutgoing(): Function[] { return this.ioSocket.listenersAnyOutgoing(); } - off(eventName?: string, listener?: Function[]) { + off(eventName?: string, listener?: Function[]): this { if (!eventName) { // Remove all listeners for all events - return this.ioSocket.offAny(); + this.ioSocket.offAny(); + return this; } if (eventName && !listener) { // Remove all listeners for that event - return this.ioSocket.off(eventName); + this.ioSocket.off(eventName); + return this; } // Removes the specified listener from the listener array for the event named - return this.ioSocket.off(eventName, listener); + this.ioSocket.off(eventName, listener); + return this; + } + + offAny(callback?: (event: string, ...args: any[]) => void): this { + this.ioSocket.offAny(callback); + return this; + } + + offAnyOutgoing(callback?: (event: string, ...args: any[]) => void): this { + this.ioSocket.offAnyOutgoing(callback); + return this; } - onAny(callback: (event: string, ...args: any[]) => void) { - return this.ioSocket.onAny(callback); + onAny(callback: (event: string, ...args: any[]) => void): this { + this.ioSocket.onAny(callback); + return this; } - onAnyOutgoing(callback: (event: string, ...args: any[]) => void) { - return this.ioSocket.onAnyOutgoing(callback); + onAnyOutgoing(callback: (event: string, ...args: any[]) => void): this { + this.ioSocket.onAnyOutgoing(callback); + return this; } - prependAny(callback: (event: string, ...args: any[]) => void) { - return this.ioSocket.prependAny(callback); + prependAny(callback: (event: string, ...args: any[]) => void): this { + this.ioSocket.prependAny(callback); + return this; } prependAnyOutgoing( callback: (event: string | symbol, ...args: any[]) => void - ) { - return this.ioSocket.prependAnyOutgoing(callback); + ): this { + this.ioSocket.prependAnyOutgoing(callback); + return this; + } + + timeout(value: number): this { + this.ioSocket.timeout(value); + return this; + } + + get volatile(): this { + // this getter has a side-effect of turning the socket instance true, + // but it returns the actual instance, so we need to get the value to force the side effect + const _ = this.ioSocket.volatile; + return this; + } + + get active(): boolean { + return this.ioSocket.active; + } + + get connected(): boolean { + return this.ioSocket.connected; + } + + get disconnected(): boolean { + return this.ioSocket.disconnected; + } + + get recovered(): boolean { + return this.ioSocket.recovered; } - timeout(value: number) { - return this.ioSocket.timeout(value); + get id(): string { + return this.ioSocket.id; } - volatile() { - return this.ioSocket.volatile; + compress(value: boolean): this { + this.ioSocket.compress(value); + return this; } }