Skip to content

Commit

Permalink
Fix and sync api wrapper (rodgc#190)
Browse files Browse the repository at this point in the history
* Fix socket.of() method

Socket-io's of() only exists in the server API and it returns a new
instance.

For client we must create a new `io()` with the URL containing the
namespace. Let's do this and share instances whenever the namespace is
reused.

* Fix chaining methods

Since we're wrapping the socket, we must return ourselves and not the
internal socket.

* Fix volatile usage

It's a getter that toggles a flag, but then we must return the actual
instance

* Add return types to help users

Since we're not importing socket.io-client typings, these are
particularly important to avoid `any`

* Add emitWithAck()

* Add offAny() and offAnyOutgoing()

These match onAny() and onAnyOutgoing()

* Add send()

Basically emit('message', ...args)

* Add compress()

* Add attributes

* Fix connect() and disconnect() arguments

According to
https://github.com/socketio/socket.io/blob/main/packages/socket.io-client/lib/socket.ts
and https://socket.io/docs/v4/client-api/
these functions do not receive any arguments.
  • Loading branch information
barbieri authored Dec 27, 2024
1 parent 64774f4 commit 0cda11e
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 37 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

Expand Down
161 changes: 126 additions & 35 deletions src/socket-io.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand All @@ -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> {
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 0cda11e

Please sign in to comment.