diff --git a/lib/broadcast-operator.ts b/lib/broadcast-operator.ts index 9264622478..2bc7271ab6 100644 --- a/lib/broadcast-operator.ts +++ b/lib/broadcast-operator.ts @@ -22,9 +22,18 @@ export class BroadcastOperator /** * Targets a room when emitting. * - * @param room - * @return a new BroadcastOperator instance - * @public + * @example + * // the “foo” event will be broadcast to all connected clients in the “room-101” room + * io.to("room-101").emit("foo", "bar"); + * + * // with an array of rooms (a client will be notified at most once) + * io.to(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * io.to("room-101").to("room-102").emit("foo", "bar"); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ public to(room: Room | Room[]) { const rooms = new Set(this.rooms); @@ -42,11 +51,14 @@ export class BroadcastOperator } /** - * Targets a room when emitting. + * Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases: * - * @param room - * @return a new BroadcastOperator instance - * @public + * @example + * // disconnect all clients in the "room-101" room + * io.in("room-101").disconnectSockets(); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ public in(room: Room | Room[]) { return this.to(room); @@ -55,9 +67,18 @@ export class BroadcastOperator /** * Excludes a room when emitting. * - * @param room - * @return a new BroadcastOperator instance - * @public + * @example + * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room + * io.except("room-101").emit("foo", "bar"); + * + * // with an array of rooms + * io.except(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * io.except("room-101").except("room-102").emit("foo", "bar"); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ public except(room: Room | Room[]) { const exceptRooms = new Set(this.exceptRooms); @@ -77,9 +98,11 @@ export class BroadcastOperator /** * Sets the compress flag. * + * @example + * io.compress(false).emit("hello"); + * * @param compress - if `true`, compresses the sending data * @return a new BroadcastOperator instance - * @public */ public compress(compress: boolean) { const flags = Object.assign({}, this.flags, { compress }); @@ -96,8 +119,10 @@ export class BroadcastOperator * receive messages (because of network slowness or other issues, or because they’re connected through long polling * and is in the middle of a request-response cycle). * + * @example + * io.volatile.emit("hello"); // the clients may or may not receive it + * * @return a new BroadcastOperator instance - * @public */ public get volatile() { const flags = Object.assign({}, this.flags, { volatile: true }); @@ -112,8 +137,11 @@ export class BroadcastOperator /** * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * - * @return a new BroadcastOperator instance - * @public + * @example + * // the “foo” event will be broadcast to all connected clients on this node + * io.local.emit("foo", "bar"); + * + * @return a new {@link BroadcastOperator} instance for chaining */ public get local() { const flags = Object.assign({}, this.flags, { local: true }); @@ -128,14 +156,15 @@ export class BroadcastOperator /** * Adds a timeout in milliseconds for the next operation * - *

-   *
+   * @example
    * io.timeout(1000).emit("some-event", (err, responses) => {
-   *   // ...
+   *   if (err) {
+   *     // some clients did not acknowledge the event in the given delay
+   *   } else {
+   *     console.log(responses); // one response per client
+   *   }
    * });
    *
-   * 
- * * @param timeout */ public timeout(timeout: number) { @@ -151,8 +180,23 @@ export class BroadcastOperator /** * Emits to all clients. * + * @example + * // the “foo” event will be broadcast to all connected clients + * io.emit("foo", "bar"); + * + * // the “foo” event will be broadcast to all connected clients in the “room-101” room + * io.to("room-101").emit("foo", "bar"); + * + * // with an acknowledgement expected from all connected clients + * io.timeout(1000).emit("some-event", (err, responses) => { + * if (err) { + * // some clients did not acknowledge the event in the given delay + * } else { + * console.log(responses); // one response per client + * } + * }); + * * @return Always true - * @public */ public emit>( ev: Ev, @@ -235,7 +279,8 @@ export class BroadcastOperator /** * Gets a list of clients. * - * @public + * @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or + * {@link fetchSockets} instead. */ public allSockets(): Promise> { if (!this.adapter) { @@ -247,9 +292,28 @@ export class BroadcastOperator } /** - * Returns the matching socket instances + * Returns the matching socket instances. This method works across a cluster of several Socket.IO servers. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * // return all Socket instances + * const sockets = await io.fetchSockets(); * - * @public + * // return all Socket instances in the "room1" room + * const sockets = await io.in("room1").fetchSockets(); + * + * for (const socket of sockets) { + * console.log(socket.id); + * console.log(socket.handshake); + * console.log(socket.rooms); + * console.log(socket.data); + * + * socket.emit("hello"); + * socket.join("room1"); + * socket.leave("room2"); + * socket.disconnect(); + * } */ public fetchSockets(): Promise[]> { return this.adapter @@ -274,10 +338,19 @@ export class BroadcastOperator } /** - * Makes the matching socket instances join the specified rooms + * Makes the matching socket instances join the specified rooms. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example * - * @param room - * @public + * // make all socket instances join the "room1" room + * io.socketsJoin("room1"); + * + * // make all socket instances in the "room1" room join the "room2" and "room3" rooms + * io.in("room1").socketsJoin(["room2", "room3"]); + * + * @param room - a room, or an array of rooms */ public socketsJoin(room: Room | Room[]): void { this.adapter.addSockets( @@ -291,10 +364,18 @@ export class BroadcastOperator } /** - * Makes the matching socket instances leave the specified rooms + * Makes the matching socket instances leave the specified rooms. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * // make all socket instances leave the "room1" room + * io.socketsLeave("room1"); + * + * // make all socket instances in the "room1" room leave the "room2" and "room3" rooms + * io.in("room1").socketsLeave(["room2", "room3"]); * - * @param room - * @public + * @param room - a room, or an array of rooms */ public socketsLeave(room: Room | Room[]): void { this.adapter.delSockets( @@ -308,10 +389,18 @@ export class BroadcastOperator } /** - * Makes the matching socket instances disconnect + * Makes the matching socket instances disconnect. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * // make all socket instances disconnect (the connections might be kept alive for other namespaces) + * io.disconnectSockets(); + * + * // make all socket instances in the "room1" room disconnect and close the underlying connections + * io.in("room1").disconnectSockets(true); * * @param close - whether to close the underlying connection - * @public */ public disconnectSockets(close: boolean = false): void { this.adapter.disconnectSockets( @@ -370,7 +459,6 @@ export class RemoteSocket * Joins a room. * * @param {String|Array} room - room or array of rooms - * @public */ public join(room: Room | Room[]): void { return this.operator.socketsJoin(room); @@ -380,7 +468,6 @@ export class RemoteSocket * Leaves a room. * * @param {String} room - * @public */ public leave(room: Room): void { return this.operator.socketsLeave(room); @@ -391,8 +478,6 @@ export class RemoteSocket * * @param {Boolean} close - if `true`, closes the underlying connection * @return {Socket} self - * - * @public */ public disconnect(close = false): this { this.operator.disconnectSockets(close); diff --git a/lib/index.ts b/lib/index.ts index 291b1c6150..f96ae20a45 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -72,6 +72,32 @@ interface ServerOptions extends EngineOptions, AttachOptions { connectTimeout: number; } +/** + * Represents a Socket.IO server. + * + * @example + * import { Server } from "socket.io"; + * + * const io = new Server(); + * + * io.on("connection", (socket) => { + * console.log(`socket ${socket.id} connected`); + * + * // send an event to the client + * socket.emit("foo", "bar"); + * + * socket.on("foobar", () => { + * // an event was received from the client + * }); + * + * // upon disconnection + * socket.on("disconnect", (reason) => { + * console.log(`socket ${socket.id} disconnected due to ${reason}`); + * }); + * }); + * + * io.listen(3000); + */ export class Server< ListenEvents extends EventsMap = DefaultEventsMap, EmitEvents extends EventsMap = ListenEvents, @@ -96,11 +122,8 @@ export class Server< /** * A reference to the underlying Engine.IO server. * - * Example: - * - * - * const clientsCount = io.engine.clientsCount; - * + * @example + * const clientsCount = io.engine.clientsCount; * */ public engine: any; @@ -139,7 +162,6 @@ export class Server< * * @param srv http server, port, or options * @param [opts] - * @public */ constructor(opts?: Partial); constructor( @@ -190,7 +212,6 @@ export class Server< * * @param v - whether to serve client code * @return self when setting or value when getting - * @public */ public serveClient(v: boolean): this; public serveClient(): boolean; @@ -253,7 +274,6 @@ export class Server< * * @param {String} v pathname * @return {Server|String} self when setting or value when getting - * @public */ public path(v: string): this; public path(): string; @@ -275,7 +295,6 @@ export class Server< /** * Set the delay after which a client without namespace is closed * @param v - * @public */ public connectTimeout(v: number): this; public connectTimeout(): number; @@ -291,7 +310,6 @@ export class Server< * * @param v pathname * @return self when setting or value when getting - * @public */ public adapter(): AdapterConstructor | undefined; public adapter(v: AdapterConstructor): this; @@ -312,7 +330,6 @@ export class Server< * @param srv - server or port * @param opts - options passed to engine.io * @return self - * @public */ public listen( srv: http.Server | HTTPSServer | number, @@ -327,7 +344,6 @@ export class Server< * @param srv - server or port * @param opts - options passed to engine.io * @return self - * @public */ public attach( srv: http.Server | HTTPSServer | number, @@ -561,7 +577,6 @@ export class Server< * * @param {engine.Server} engine engine.io (or compatible) server * @return self - * @public */ public bind(engine): this { this.engine = engine; @@ -589,9 +604,20 @@ export class Server< /** * Looks up a namespace. * - * @param {String|RegExp|Function} name nsp name + * @example + * // with a simple string + * const myNamespace = io.of("/my-namespace"); + * + * // with a regex + * const dynamicNsp = io.of(/^\/dynamic-\d+$/).on("connection", (socket) => { + * const namespace = socket.nsp; // newNamespace.name === "/dynamic-101" + * + * // broadcast to all clients in the given sub-namespace + * namespace.emit("hello"); + * }); + * + * @param name - nsp name * @param fn optional, nsp `connection` ev handler - * @public */ public of( name: string | RegExp | ParentNspNameMatchFn, @@ -637,7 +663,6 @@ export class Server< * Closes server connection * * @param [fn] optional, called as `fn([err])` on error OR all conns closed - * @public */ public close(fn?: (err?: Error) => void): void { for (const socket of this.sockets.sockets.values()) { @@ -657,10 +682,15 @@ export class Server< } /** - * Sets up namespace middleware. + * Registers a middleware, which is a function that gets executed for every incoming {@link Socket}. * - * @return self - * @public + * @example + * io.use((socket, next) => { + * // ... + * next(); + * }); + * + * @param fn - the middleware function */ public use( fn: ( @@ -675,43 +705,71 @@ export class Server< /** * Targets a room when emitting. * - * @param room - * @return self - * @public + * @example + * // the “foo” event will be broadcast to all connected clients in the “room-101” room + * io.to("room-101").emit("foo", "bar"); + * + * // with an array of rooms (a client will be notified at most once) + * io.to(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * io.to("room-101").to("room-102").emit("foo", "bar"); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ - public to(room: Room | Room[]): BroadcastOperator { + public to(room: Room | Room[]) { return this.sockets.to(room); } /** - * Targets a room when emitting. + * Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases: * - * @param room - * @return self - * @public + * @example + * // disconnect all clients in the "room-101" room + * io.in("room-101").disconnectSockets(); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ - public in(room: Room | Room[]): BroadcastOperator { + public in(room: Room | Room[]) { return this.sockets.in(room); } /** * Excludes a room when emitting. * - * @param name - * @return self - * @public + * @example + * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room + * io.except("room-101").emit("foo", "bar"); + * + * // with an array of rooms + * io.except(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * io.except("room-101").except("room-102").emit("foo", "bar"); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ - public except( - name: Room | Room[] - ): BroadcastOperator { - return this.sockets.except(name); + public except(room: Room | Room[]) { + return this.sockets.except(room); } /** * Sends a `message` event to all clients. * + * This method mimics the WebSocket.send() method. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send + * + * @example + * io.send("hello"); + * + * // this is equivalent to + * io.emit("message", "hello"); + * * @return self - * @public */ public send(...args: EventParams): this { this.sockets.emit("message", ...args); @@ -719,10 +777,9 @@ export class Server< } /** - * Sends a `message` event to all clients. + * Sends a `message` event to all clients. Alias of {@link send}. * * @return self - * @public */ public write(...args: EventParams): this { this.sockets.emit("message", ...args); @@ -730,11 +787,30 @@ export class Server< } /** - * Emit a packet to other Socket.IO servers + * Sends a message to the other Socket.IO servers of the cluster. + * + * @example + * io.serverSideEmit("hello", "world"); + * + * io.on("hello", (arg1) => { + * console.log(arg1); // prints "world" + * }); + * + * // acknowledgements (without binary content) are supported too: + * io.serverSideEmit("ping", (err, responses) => { + * if (err) { + * // some clients did not acknowledge the event in the given delay + * } else { + * console.log(responses); // one response per client + * } + * }); + * + * io.on("ping", (cb) => { + * cb("pong"); + * }); * * @param ev - the event name * @param args - an array of arguments, which may include an acknowledgement callback at the end - * @public */ public serverSideEmit>( ev: Ev, @@ -748,8 +824,6 @@ export class Server< * * @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or * {@link Server#fetchSockets} instead. - * - * @public */ public allSockets(): Promise> { return this.sockets.allSockets(); @@ -758,13 +832,13 @@ export class Server< /** * Sets the compress flag. * + * @example + * io.compress(false).emit("hello"); + * * @param compress - if `true`, compresses the sending data - * @return self - * @public + * @return a new {@link BroadcastOperator} instance for chaining */ - public compress( - compress: boolean - ): BroadcastOperator { + public compress(compress: boolean) { return this.sockets.compress(compress); } @@ -773,34 +847,40 @@ export class Server< * receive messages (because of network slowness or other issues, or because they’re connected through long polling * and is in the middle of a request-response cycle). * - * @return self - * @public + * @example + * io.volatile.emit("hello"); // the clients may or may not receive it + * + * @return a new {@link BroadcastOperator} instance for chaining */ - public get volatile(): BroadcastOperator { + public get volatile() { return this.sockets.volatile; } /** * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * - * @return self - * @public + * @example + * // the “foo” event will be broadcast to all connected clients on this node + * io.local.emit("foo", "bar"); + * + * @return a new {@link BroadcastOperator} instance for chaining */ - public get local(): BroadcastOperator { + public get local() { return this.sockets.local; } /** - * Adds a timeout in milliseconds for the next operation - * - *

+   * Adds a timeout in milliseconds for the next operation.
    *
+   * @example
    * io.timeout(1000).emit("some-event", (err, responses) => {
-   *   // ...
+   *   if (err) {
+   *     // some clients did not acknowledge the event in the given delay
+   *   } else {
+   *     console.log(responses); // one response per client
+   *   }
    * });
    *
-   * 
- * * @param timeout */ public timeout(timeout: number) { @@ -808,39 +888,83 @@ export class Server< } /** - * Returns the matching socket instances + * Returns the matching socket instances. * - * @public + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * // return all Socket instances + * const sockets = await io.fetchSockets(); + * + * // return all Socket instances in the "room1" room + * const sockets = await io.in("room1").fetchSockets(); + * + * for (const socket of sockets) { + * console.log(socket.id); + * console.log(socket.handshake); + * console.log(socket.rooms); + * console.log(socket.data); + * + * socket.emit("hello"); + * socket.join("room1"); + * socket.leave("room2"); + * socket.disconnect(); + * } */ public fetchSockets(): Promise[]> { return this.sockets.fetchSockets(); } /** - * Makes the matching socket instances join the specified rooms + * Makes the matching socket instances join the specified rooms. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * + * // make all socket instances join the "room1" room + * io.socketsJoin("room1"); * - * @param room - * @public + * // make all socket instances in the "room1" room join the "room2" and "room3" rooms + * io.in("room1").socketsJoin(["room2", "room3"]); + * + * @param room - a room, or an array of rooms */ public socketsJoin(room: Room | Room[]) { return this.sockets.socketsJoin(room); } /** - * Makes the matching socket instances leave the specified rooms + * Makes the matching socket instances leave the specified rooms. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * // make all socket instances leave the "room1" room + * io.socketsLeave("room1"); + * + * // make all socket instances in the "room1" room leave the "room2" and "room3" rooms + * io.in("room1").socketsLeave(["room2", "room3"]); * - * @param room - * @public + * @param room - a room, or an array of rooms */ public socketsLeave(room: Room | Room[]) { return this.sockets.socketsLeave(room); } /** - * Makes the matching socket instances disconnect + * Makes the matching socket instances disconnect. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * // make all socket instances disconnect (the connections might be kept alive for other namespaces) + * io.disconnectSockets(); + * + * // make all socket instances in the "room1" room disconnect and close the underlying connections + * io.in("room1").disconnectSockets(true); * * @param close - whether to close the underlying connection - * @public */ public disconnectSockets(close: boolean = false) { return this.sockets.disconnectSockets(close); diff --git a/lib/namespace.ts b/lib/namespace.ts index 0f1b28a8e0..620ead42e6 100644 --- a/lib/namespace.ts +++ b/lib/namespace.ts @@ -52,6 +52,59 @@ export const RESERVED_EVENTS: ReadonlySet = new Set< keyof ServerReservedEventsMap >(["connect", "connection", "new_namespace"]); +/** + * A Namespace is a communication channel that allows you to split the logic of your application over a single shared + * connection. + * + * Each namespace has its own: + * + * - event handlers + * + * ``` + * io.of("/orders").on("connection", (socket) => { + * socket.on("order:list", () => {}); + * socket.on("order:create", () => {}); + * }); + * + * io.of("/users").on("connection", (socket) => { + * socket.on("user:list", () => {}); + * }); + * ``` + * + * - rooms + * + * ``` + * const orderNamespace = io.of("/orders"); + * + * orderNamespace.on("connection", (socket) => { + * socket.join("room1"); + * orderNamespace.to("room1").emit("hello"); + * }); + * + * const userNamespace = io.of("/users"); + * + * userNamespace.on("connection", (socket) => { + * socket.join("room1"); // distinct from the room in the "orders" namespace + * userNamespace.to("room1").emit("holà"); + * }); + * ``` + * + * - middlewares + * + * ``` + * const orderNamespace = io.of("/orders"); + * + * orderNamespace.use((socket, next) => { + * // ensure the socket has access to the "orders" namespace + * }); + * + * const userNamespace = io.of("/users"); + * + * userNamespace.use((socket, next) => { + * // ensure the socket has access to the "users" namespace + * }); + * ``` + */ export class Namespace< ListenEvents extends EventsMap = DefaultEventsMap, EmitEvents extends EventsMap = ListenEvents, @@ -123,10 +176,17 @@ export class Namespace< } /** - * Sets up namespace middleware. + * Registers a middleware, which is a function that gets executed for every incoming {@link Socket}. * - * @return self - * @public + * @example + * const myNamespace = io.of("/my-namespace"); + * + * myNamespace.use((socket, next) => { + * // ... + * next(); + * }); + * + * @param fn - the middleware function */ public use( fn: ( @@ -171,20 +231,36 @@ export class Namespace< /** * Targets a room when emitting. * - * @param room - * @return self - * @public + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // the “foo” event will be broadcast to all connected clients in the “room-101” room + * myNamespace.to("room-101").emit("foo", "bar"); + * + * // with an array of rooms (a client will be notified at most once) + * myNamespace.to(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * myNamespace.to("room-101").to("room-102").emit("foo", "bar"); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ public to(room: Room | Room[]) { return new BroadcastOperator(this.adapter).to(room); } /** - * Targets a room when emitting. + * Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases: * - * @param room - * @return self - * @public + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // disconnect all clients in the "room-101" room + * myNamespace.in("room-101").disconnectSockets(); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ public in(room: Room | Room[]) { return new BroadcastOperator(this.adapter).in(room); @@ -193,9 +269,20 @@ export class Namespace< /** * Excludes a room when emitting. * - * @param room - * @return self - * @public + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room + * myNamespace.except("room-101").emit("foo", "bar"); + * + * // with an array of rooms + * myNamespace.except(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * myNamespace.except("room-101").except("room-102").emit("foo", "bar"); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ public except(room: Room | Room[]) { return new BroadcastOperator(this.adapter).except( @@ -271,10 +358,26 @@ export class Namespace< } /** - * Emits to all clients. + * Emits to all connected clients. + * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * myNamespace.emit("hello", "world"); + * + * // all serializable datastructures are supported (no need to call JSON.stringify) + * myNamespace.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) }); + * + * // with an acknowledgement from the clients + * myNamespace.timeout(1000).emit("some-event", (err, responses) => { + * if (err) { + * // some clients did not acknowledge the event in the given delay + * } else { + * console.log(responses); // one response per client + * } + * }); * * @return Always true - * @public */ public emit>( ev: Ev, @@ -289,8 +392,19 @@ export class Namespace< /** * Sends a `message` event to all clients. * + * This method mimics the WebSocket.send() method. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send + * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * myNamespace.send("hello"); + * + * // this is equivalent to + * myNamespace.emit("message", "hello"); + * * @return self - * @public */ public send(...args: EventParams): this { this.emit("message", ...args); @@ -298,10 +412,9 @@ export class Namespace< } /** - * Sends a `message` event to all clients. + * Sends a `message` event to all clients. Sends a `message` event. Alias of {@link send}. * * @return self - * @public */ public write(...args: EventParams): this { this.emit("message", ...args); @@ -309,11 +422,32 @@ export class Namespace< } /** - * Emit a packet to other Socket.IO servers + * Sends a message to the other Socket.IO servers of the cluster. + * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * myNamespace.serverSideEmit("hello", "world"); + * + * myNamespace.on("hello", (arg1) => { + * console.log(arg1); // prints "world" + * }); + * + * // acknowledgements (without binary content) are supported too: + * myNamespace.serverSideEmit("ping", (err, responses) => { + * if (err) { + * // some clients did not acknowledge the event in the given delay + * } else { + * console.log(responses); // one response per client + * } + * }); + * + * myNamespace.on("ping", (cb) => { + * cb("pong"); + * }); * * @param ev - the event name * @param args - an array of arguments, which may include an acknowledgement callback at the end - * @public */ public serverSideEmit>( ev: Ev, @@ -343,8 +477,6 @@ export class Namespace< * * @deprecated this method will be removed in the next major release, please use {@link Namespace#serverSideEmit} or * {@link Namespace#fetchSockets} instead. - * - * @public */ public allSockets(): Promise> { return new BroadcastOperator( @@ -355,9 +487,13 @@ export class Namespace< /** * Sets the compress flag. * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * myNamespace.compress(false).emit("hello"); + * * @param compress - if `true`, compresses the sending data * @return self - * @public */ public compress(compress: boolean) { return new BroadcastOperator(this.adapter).compress( @@ -370,8 +506,12 @@ export class Namespace< * receive messages (because of network slowness or other issues, or because they’re connected through long polling * and is in the middle of a request-response cycle). * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * myNamespace.volatile.emit("hello"); // the clients may or may not receive it + * * @return self - * @public */ public get volatile() { return new BroadcastOperator(this.adapter).volatile; @@ -380,24 +520,32 @@ export class Namespace< /** * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * - * @return self - * @public + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // the “foo” event will be broadcast to all connected clients on this node + * myNamespace.local.emit("foo", "bar"); + * + * @return a new {@link BroadcastOperator} instance for chaining */ public get local() { return new BroadcastOperator(this.adapter).local; } /** - * Adds a timeout in milliseconds for the next operation + * Adds a timeout in milliseconds for the next operation. * - *

+   * @example
+   * const myNamespace = io.of("/my-namespace");
    *
-   * io.timeout(1000).emit("some-event", (err, responses) => {
-   *   // ...
+   * myNamespace.timeout(1000).emit("some-event", (err, responses) => {
+   *   if (err) {
+   *     // some clients did not acknowledge the event in the given delay
+   *   } else {
+   *     console.log(responses); // one response per client
+   *   }
    * });
    *
-   * 
- * * @param timeout */ public timeout(timeout: number) { @@ -407,9 +555,30 @@ export class Namespace< } /** - * Returns the matching socket instances + * Returns the matching socket instances. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. * - * @public + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // return all Socket instances + * const sockets = await myNamespace.fetchSockets(); + * + * // return all Socket instances in the "room1" room + * const sockets = await myNamespace.in("room1").fetchSockets(); + * + * for (const socket of sockets) { + * console.log(socket.id); + * console.log(socket.handshake); + * console.log(socket.rooms); + * console.log(socket.data); + * + * socket.emit("hello"); + * socket.join("room1"); + * socket.leave("room2"); + * socket.disconnect(); + * } */ public fetchSockets() { return new BroadcastOperator( @@ -418,10 +587,20 @@ export class Namespace< } /** - * Makes the matching socket instances join the specified rooms + * Makes the matching socket instances join the specified rooms. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // make all socket instances join the "room1" room + * myNamespace.socketsJoin("room1"); + * + * // make all socket instances in the "room1" room join the "room2" and "room3" rooms + * myNamespace.in("room1").socketsJoin(["room2", "room3"]); * - * @param room - * @public + * @param room - a room, or an array of rooms */ public socketsJoin(room: Room | Room[]) { return new BroadcastOperator( @@ -430,10 +609,20 @@ export class Namespace< } /** - * Makes the matching socket instances leave the specified rooms + * Makes the matching socket instances leave the specified rooms. * - * @param room - * @public + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // make all socket instances leave the "room1" room + * myNamespace.socketsLeave("room1"); + * + * // make all socket instances in the "room1" room leave the "room2" and "room3" rooms + * myNamespace.in("room1").socketsLeave(["room2", "room3"]); + * + * @param room - a room, or an array of rooms */ public socketsLeave(room: Room | Room[]) { return new BroadcastOperator( @@ -442,10 +631,20 @@ export class Namespace< } /** - * Makes the matching socket instances disconnect + * Makes the matching socket instances disconnect. + * + * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}. + * + * @example + * const myNamespace = io.of("/my-namespace"); + * + * // make all socket instances disconnect (the connections might be kept alive for other namespaces) + * myNamespace.disconnectSockets(); + * + * // make all socket instances in the "room1" room disconnect and close the underlying connections + * myNamespace.in("room1").disconnectSockets(true); * * @param close - whether to close the underlying connection - * @public */ public disconnectSockets(close: boolean = false) { return new BroadcastOperator( diff --git a/lib/socket.ts b/lib/socket.ts index c9079efa37..6c4cb45ea7 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -128,6 +128,37 @@ export type Event = [string, ...any[]]; function noop() {} +/** + * This is the main object for interacting with a client. + * + * A Socket belongs to a given {@link Namespace} and uses an underlying {@link Client} to communicate. + * + * Within each {@link Namespace}, you can also define arbitrary channels (called "rooms") that the {@link Socket} can + * join and leave. That provides a convenient way to broadcast to a group of socket instances. + * + * @example + * io.on("connection", (socket) => { + * console.log(`socket ${socket.id} connected`); + * + * // send an event to the client + * socket.emit("foo", "bar"); + * + * socket.on("foobar", () => { + * // an event was received from the client + * }); + * + * // join the room named "room1" + * socket.join("room1"); + * + * // broadcast to everyone in the room named "room1" + * io.to("room1").emit("hello"); + * + * // upon disconnection + * socket.on("disconnect", (reason) => { + * console.log(`socket ${socket.id} disconnected due to ${reason}`); + * }); + * }); + */ export class Socket< ListenEvents extends EventsMap = DefaultEventsMap, EmitEvents extends EventsMap = ListenEvents, @@ -138,13 +169,32 @@ export class Socket< EmitEvents, SocketReservedEventsMap > { + /** + * An unique identifier for the session. + */ public readonly id: SocketId; + /** + * The handshake details. + */ public readonly handshake: Handshake; /** - * Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method + * Additional information that can be attached to the Socket instance and which will be used in the + * {@link Server.fetchSockets()} method. */ public data: Partial = {}; - + /** + * Whether the socket is currently connected or not. + * + * @example + * io.use((socket, next) => { + * console.log(socket.connected); // false + * next(); + * }); + * + * io.on("connection", (socket) => { + * console.log(socket.connected); // true + * }); + */ public connected: boolean = false; private readonly server: Server< @@ -209,8 +259,20 @@ export class Socket< /** * Emits to this client. * + * @example + * io.on("connection", (socket) => { + * socket.emit("hello", "world"); + * + * // all serializable datastructures are supported (no need to call JSON.stringify) + * socket.emit("hello", 1, "2", { 3: ["4"], 5: Buffer.from([6]) }); + * + * // with an acknowledgement from the client + * socket.emit("hello", "world", (val) => { + * // ... + * }); + * }); + * * @return Always returns `true`. - * @public */ public emit>( ev: Ev, @@ -268,43 +330,83 @@ export class Socket< /** * Targets a room when broadcasting. * - * @param room - * @return self - * @public + * @example + * io.on("connection", (socket) => { + * // the “foo” event will be broadcast to all connected clients in the “room-101” room, except this socket + * socket.to("room-101").emit("foo", "bar"); + * + * // the code above is equivalent to: + * io.to("room-101").except(socket.id).emit("foo", "bar"); + * + * // with an array of rooms (a client will be notified at most once) + * socket.to(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * socket.to("room-101").to("room-102").emit("foo", "bar"); + * }); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ - public to(room: Room | Room[]): BroadcastOperator { + public to(room: Room | Room[]) { return this.newBroadcastOperator().to(room); } /** - * Targets a room when broadcasting. + * Targets a room when broadcasting. Similar to `to()`, but might feel clearer in some cases: * - * @param room - * @return self - * @public + * @example + * io.on("connection", (socket) => { + * // disconnect all clients in the "room-101" room, except this socket + * socket.in("room-101").disconnectSockets(); + * }); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ - public in(room: Room | Room[]): BroadcastOperator { + public in(room: Room | Room[]) { return this.newBroadcastOperator().in(room); } /** * Excludes a room when broadcasting. * - * @param room - * @return self - * @public + * @example + * io.on("connection", (socket) => { + * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room + * // and this socket + * socket.except("room-101").emit("foo", "bar"); + * + * // with an array of rooms + * socket.except(["room-101", "room-102"]).emit("foo", "bar"); + * + * // with multiple chained calls + * socket.except("room-101").except("room-102").emit("foo", "bar"); + * }); + * + * @param room - a room, or an array of rooms + * @return a new {@link BroadcastOperator} instance for chaining */ - public except( - room: Room | Room[] - ): BroadcastOperator { + public except(room: Room | Room[]) { return this.newBroadcastOperator().except(room); } /** * Sends a `message` event. * + * This method mimics the WebSocket.send() method. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send + * + * @example + * io.on("connection", (socket) => { + * socket.send("hello"); + * + * // this is equivalent to + * socket.emit("message", "hello"); + * }); + * * @return self - * @public */ public send(...args: EventParams): this { this.emit("message", ...args); @@ -312,10 +414,9 @@ export class Socket< } /** - * Sends a `message` event. + * Sends a `message` event. Alias of {@link send}. * * @return self - * @public */ public write(...args: EventParams): this { this.emit("message", ...args); @@ -341,9 +442,17 @@ export class Socket< /** * Joins a room. * + * @example + * io.on("connection", (socket) => { + * // join a single room + * socket.join("room1"); + * + * // join multiple rooms + * socket.join(["room1", "room2"]); + * }); + * * @param {String|Array} rooms - room or array of rooms * @return a Promise or nothing, depending on the adapter - * @public */ public join(rooms: Room | Array): Promise | void { debug("join room %s", rooms); @@ -357,9 +466,17 @@ export class Socket< /** * Leaves a room. * + * @example + * io.on("connection", (socket) => { + * // leave a single room + * socket.leave("room1"); + * + * // leave multiple rooms + * socket.leave("room1").leave("room2"); + * }); + * * @param {String} room * @return a Promise or nothing, depending on the adapter - * @public */ public leave(room: string): Promise | void { debug("leave room %s", room); @@ -559,10 +676,17 @@ export class Socket< /** * Disconnects this client. * - * @param {Boolean} close - if `true`, closes the underlying connection - * @return {Socket} self + * @example + * io.on("connection", (socket) => { + * // disconnect this socket (the connection might be kept alive for other namespaces) + * socket.disconnect(); + * + * // disconnect this socket and close the underlying connection + * socket.disconnect(true); + * }) * - * @public + * @param {Boolean} close - if `true`, closes the underlying connection + * @return self */ public disconnect(close = false): this { if (!this.connected) return this; @@ -578,9 +702,13 @@ export class Socket< /** * Sets the compress flag. * + * @example + * io.on("connection", (socket) => { + * socket.compress(false).emit("hello"); + * }); + * * @param {Boolean} compress - if `true`, compresses the sending data * @return {Socket} self - * @public */ public compress(compress: boolean): this { this.flags.compress = compress; @@ -592,8 +720,12 @@ export class Socket< * receive messages (because of network slowness or other issues, or because they’re connected through long polling * and is in the middle of a request-response cycle). * + * @example + * io.on("connection", (socket) => { + * socket.volatile.emit("hello"); // the client may or may not receive it + * }); + * * @return {Socket} self - * @public */ public get volatile(): this { this.flags.volatile = true; @@ -604,20 +736,30 @@ export class Socket< * Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the * sender. * - * @return {Socket} self - * @public + * @example + * io.on("connection", (socket) => { + * // the “foo” event will be broadcast to all connected clients, except this socket + * socket.broadcast.emit("foo", "bar"); + * }); + * + * @return a new {@link BroadcastOperator} instance for chaining */ - public get broadcast(): BroadcastOperator { + public get broadcast() { return this.newBroadcastOperator(); } /** * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * - * @return {Socket} self - * @public + * @example + * io.on("connection", (socket) => { + * // the “foo” event will be broadcast to all connected clients on this node, except this socket + * socket.local.emit("foo", "bar"); + * }); + * + * @return a new {@link BroadcastOperator} instance for chaining */ - public get local(): BroadcastOperator { + public get local() { return this.newBroadcastOperator().local; } @@ -625,16 +767,16 @@ export class Socket< * Sets a modifier for a subsequent event emission that the callback will be called with an error when the * given number of milliseconds have elapsed without an acknowledgement from the client: * - * ``` - * socket.timeout(5000).emit("my-event", (err) => { - * if (err) { - * // the client did not acknowledge the event in the given delay - * } + * @example + * io.on("connection", (socket) => { + * socket.timeout(5000).emit("my-event", (err) => { + * if (err) { + * // the client did not acknowledge the event in the given delay + * } + * }); * }); - * ``` * * @returns self - * @public */ public timeout(timeout: number): this { this.flags.timeout = timeout; @@ -666,9 +808,25 @@ export class Socket< /** * Sets up socket middleware. * + * @example + * io.on("connection", (socket) => { + * socket.use(([event, ...args], next) => { + * if (isUnauthorized(event)) { + * return next(new Error("unauthorized event")); + * } + * // do not forget to call next + * next(); + * }); + * + * socket.on("error", (err) => { + * if (err && err.message === "unauthorized event") { + * socket.disconnect(); + * } + * }); + * }); + * * @param {Function} fn - middleware function (event, next) * @return {Socket} self - * @public */ public use(fn: (event: Event, next: (err?: Error) => void) => void): this { this.fns.push(fn); @@ -711,8 +869,6 @@ export class Socket< /** * A reference to the request that originated the underlying Engine.IO Socket. - * - * @public */ public get request(): IncomingMessage { return this.client.request; @@ -721,14 +877,30 @@ export class Socket< /** * A reference to the underlying Client transport connection (Engine.IO Socket object). * - * @public + * @example + * io.on("connection", (socket) => { + * console.log(socket.conn.transport.name); // prints "polling" or "websocket" + * + * socket.conn.once("upgrade", () => { + * console.log(socket.conn.transport.name); // prints "websocket" + * }); + * }); */ public get conn() { return this.client.conn; } /** - * @public + * Returns the rooms the socket is currently in. + * + * @example + * io.on("connection", (socket) => { + * console.log(socket.rooms); // Set { } + * + * socket.join("room1"); + * + * console.log(socket.rooms); // Set { , "room1" } + * }); */ public get rooms(): Set { return this.adapter.socketRooms(this.id) || new Set(); @@ -738,8 +910,14 @@ export class Socket< * Adds a listener that will be fired when any event is received. The event name is passed as the first argument to * the callback. * + * @example + * io.on("connection", (socket) => { + * socket.onAny((event, ...args) => { + * console.log(`got event ${event}`); + * }); + * }); + * * @param listener - * @public */ public onAny(listener: (...args: any[]) => void): this { this._anyListeners = this._anyListeners || []; @@ -752,7 +930,6 @@ export class Socket< * the callback. The listener is added to the beginning of the listeners array. * * @param listener - * @public */ public prependAny(listener: (...args: any[]) => void): this { this._anyListeners = this._anyListeners || []; @@ -763,8 +940,22 @@ export class Socket< /** * Removes the listener that will be fired when any event is received. * + * @example + * io.on("connection", (socket) => { + * const catchAllListener = (event, ...args) => { + * console.log(`got event ${event}`); + * } + * + * socket.onAny(catchAllListener); + * + * // remove a specific listener + * socket.offAny(catchAllListener); + * + * // or remove all listeners + * socket.offAny(); + * }); + * * @param listener - * @public */ public offAny(listener?: (...args: any[]) => void): this { if (!this._anyListeners) { @@ -787,28 +978,25 @@ export class Socket< /** * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, * e.g. to remove listeners. - * - * @public */ public listenersAny() { return this._anyListeners || []; } /** - * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the - * callback. - * - * @param listener + * Adds a listener that will be fired when any event is sent. The event name is passed as the first argument to + * the callback. * - *

+   * Note: acknowledgements sent to the client are not included.
    *
-   * socket.onAnyOutgoing((event, ...args) => {
-   *   console.log(event);
+   * @example
+   * io.on("connection", (socket) => {
+   *   socket.onAnyOutgoing((event, ...args) => {
+   *     console.log(`sent event ${event}`);
+   *   });
    * });
    *
-   * 
- * - * @public + * @param listener */ public onAnyOutgoing(listener: (...args: any[]) => void): this { this._anyOutgoingListeners = this._anyOutgoingListeners || []; @@ -820,17 +1008,14 @@ export class Socket< * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * callback. The listener is added to the beginning of the listeners array. * - * @param listener - * - *

-   *
-   * socket.prependAnyOutgoing((event, ...args) => {
-   *   console.log(event);
+   * @example
+   * io.on("connection", (socket) => {
+   *   socket.prependAnyOutgoing((event, ...args) => {
+   *     console.log(`sent event ${event}`);
+   *   });
    * });
    *
-   * 
- * - * @public + * @param listener */ public prependAnyOutgoing(listener: (...args: any[]) => void): this { this._anyOutgoingListeners = this._anyOutgoingListeners || []; @@ -839,24 +1024,24 @@ export class Socket< } /** - * Removes the listener that will be fired when any event is emitted. + * Removes the listener that will be fired when any event is sent. * - * @param listener - * - *

-   *
-   * const handler = (event, ...args) => {
-   *   console.log(event);
-   * }
+   * @example
+   * io.on("connection", (socket) => {
+   *   const catchAllListener = (event, ...args) => {
+   *     console.log(`sent event ${event}`);
+   *   }
    *
-   * socket.onAnyOutgoing(handler);
+   *   socket.onAnyOutgoing(catchAllListener);
    *
-   * // then later
-   * socket.offAnyOutgoing(handler);
+   *   // remove a specific listener
+   *   socket.offAnyOutgoing(catchAllListener);
    *
-   * 
+ * // or remove all listeners + * socket.offAnyOutgoing(); + * }); * - * @public + * @param listener - the catch-all listener */ public offAnyOutgoing(listener?: (...args: any[]) => void): this { if (!this._anyOutgoingListeners) { @@ -879,8 +1064,6 @@ export class Socket< /** * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, * e.g. to remove listeners. - * - * @public */ public listenersAnyOutgoing() { return this._anyOutgoingListeners || [];