Skip to content

Commit

Permalink
feat: add some utility methods
Browse files Browse the repository at this point in the history
This commit adds the following methods:

- fetchSockets: returns the matching socket instances

Syntax:

```js
// return all Socket instances
const sockets = await io.fetchSockets();

// return all Socket instances of the "admin" namespace in the "room1" room
const sockets = await io.of("/admin").in("room1").fetchSockets();
```

- socketsJoin: makes the matching socket instances join the specified rooms

Syntax:

```js
// make all Socket instances join the "room1" room
io.socketsJoin("room1");

// make all Socket instances of the "admin" namespace in the "room1" room join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");
```

- socketsLeave: makes the matching socket instances leave the specified rooms

Syntax:

```js
// make all Socket instances leave the "room1" room
io.socketsLeave("room1");

// make all Socket instances of the "admin" namespace in the "room1" room leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");
```

- disconnectSockets: makes the matching socket instances disconnect

Syntax:

```js
// make all Socket instances disconnect
io.disconnectSockets();

// make all Socket instances of the "admin" namespace in the "room1" room disconnect
io.of("/admin").in("room1").disconnectSockets();
```

Those methods share the same semantics as broadcasting. They will also
work with multiple Socket.IO servers when using the Redis adapter. In
that case, the fetchSockets() method will return a list of RemoteSocket
instances, which expose a subset of the methods and attributes of the
Socket class (the "request" attribute cannot be mocked, for example).

Related:

- #3042
- #3418
- #3570
- socketio/socket.io-redis-adapter#283
  • Loading branch information
darrachequesne committed Mar 2, 2021
1 parent 085d1de commit b25495c
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 4 deletions.
140 changes: 139 additions & 1 deletion lib/broadcast-operator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter";
import { RESERVED_EVENTS } from "./socket";
import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
import { PacketType } from "socket.io-parser";
import type { Adapter } from "socket.io-adapter";

Expand Down Expand Up @@ -160,4 +160,142 @@ export class BroadcastOperator {
}
return this.adapter.sockets(this.rooms);
}

/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return (socket as unknown) as RemoteSocket;
} else {
return new RemoteSocket(this.adapter, socket as SocketDetails);
}
});
});
}

/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
);
}

/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
);
}

/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
close
);
}
}

/**
* Format of the data when the Socket instance exists on another Socket.IO server
*/
interface SocketDetails {
id: SocketId;
handshake: Handshake;
rooms: Room[];
data: any;
}

/**
* Expose of subset of the attributes and methods of the Socket class
*/
export class RemoteSocket {
public readonly id: SocketId;
public readonly handshake: Handshake;
public readonly rooms: Set<Room>;
public readonly data: any;

private readonly operator: BroadcastOperator;

constructor(adapter: Adapter, details: SocketDetails) {
this.id = details.id;
this.handshake = details.handshake;
this.rooms = new Set(details.rooms);
this.data = details.data;
this.operator = new BroadcastOperator(adapter, new Set([this.id]));
}

public emit(ev: string, ...args: any[]): boolean {
return this.operator.emit(ev, ...args);
}

/**
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room);
}

/**
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room);
}

/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close);
return this;
}
}
43 changes: 41 additions & 2 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import debugModule from "debug";
import { Socket } from "./socket";
import type { CookieSerializeOptions } from "cookie";
import type { CorsOptions } from "cors";
import type { BroadcastOperator } from "./broadcast-operator";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";

const debug = debugModule("socket.io:server");

Expand Down Expand Up @@ -719,6 +719,45 @@ export class Server extends EventEmitter {
public get local(): BroadcastOperator {
return this.sockets.local;
}

/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket[]> {
return this.sockets.fetchSockets();
}

/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return this.sockets.socketsJoin(room);
}

/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return this.sockets.socketsLeave(room);
}

/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return this.sockets.disconnectSockets(close);
}
}

/**
Expand All @@ -740,4 +779,4 @@ emitterMethods.forEach(function (fn) {
module.exports = (srv?, opts?) => new Server(srv, opts);
module.exports.Server = Server;

export { Socket, ServerOptions, Namespace };
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket };
41 changes: 40 additions & 1 deletion lib/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Client } from "./client";
import { EventEmitter } from "events";
import debugModule from "debug";
import type { Adapter, Room, SocketId } from "socket.io-adapter";
import { BroadcastOperator } from "./broadcast-operator";
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator";

const debug = debugModule("socket.io:namespace");

Expand Down Expand Up @@ -257,4 +257,43 @@ export class Namespace extends EventEmitter {
public get local(): BroadcastOperator {
return new BroadcastOperator(this.adapter).local;
}

/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket[]> {
return new BroadcastOperator(this.adapter).fetchSockets();
}

/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room);
}

/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsLeave(room);
}

/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close);
}
}
4 changes: 4 additions & 0 deletions lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export interface Handshake {
export class Socket extends EventEmitter {
public readonly id: SocketId;
public readonly handshake: Handshake;
/**
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
*/
public data: any = {};

public connected: boolean;
public disconnected: boolean;
Expand Down
1 change: 1 addition & 0 deletions test/socket.io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as io_v2 from "socket.io-client-v2";
const ioc = require("socket.io-client");

import "./support/util";
import "./utility-methods";

// Creates a socket.io client for the given server
function client(srv, nsp?: string | object, opts?: object) {
Expand Down
Loading

0 comments on commit b25495c

Please sign in to comment.