Skip to content

Commit ac9e8ca

Browse files
fix: make io.to(...) immutable
Previously, broadcasting to a given room (by calling `io.to()`) would mutate the io instance, which could lead to surprising behaviors, like: ```js io.to("room1"); io.to("room2").emit(...); // also sent to room1 // or with async/await io.to("room3").emit("details", await fetchDetails()); // random behavior: maybe in room3, maybe to all clients ``` Calling `io.to()` (or any other broadcast modifier) will now return an immutable instance. Related: - #3431 - #3444
1 parent 7de2e87 commit ac9e8ca

File tree

6 files changed

+269
-136
lines changed

6 files changed

+269
-136
lines changed

lib/broadcast-operator.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter";
2+
import { RESERVED_EVENTS } from "./socket";
3+
import { PacketType } from "socket.io-parser";
4+
import type { Adapter } from "socket.io-adapter";
5+
6+
export class BroadcastOperator {
7+
constructor(
8+
private readonly adapter: Adapter,
9+
private readonly rooms: Set<Room> = new Set<Room>(),
10+
private readonly exceptRooms: Set<Room> = new Set<Room>(),
11+
private readonly flags: BroadcastFlags = {}
12+
) {}
13+
14+
/**
15+
* Targets a room when emitting.
16+
*
17+
* @param room
18+
* @return a new BroadcastOperator instance
19+
* @public
20+
*/
21+
public to(room: Room): BroadcastOperator {
22+
return new BroadcastOperator(
23+
this.adapter,
24+
new Set([...this.rooms, room]),
25+
this.exceptRooms,
26+
this.flags
27+
);
28+
}
29+
30+
/**
31+
* Targets a room when emitting.
32+
*
33+
* @param room
34+
* @return a new BroadcastOperator instance
35+
* @public
36+
*/
37+
public in(room: Room): BroadcastOperator {
38+
return this.to(room);
39+
}
40+
41+
/**
42+
* Excludes a room when emitting.
43+
*
44+
* @param room
45+
* @return a new BroadcastOperator instance
46+
* @public
47+
*/
48+
public except(room: Room): BroadcastOperator {
49+
return new BroadcastOperator(
50+
this.adapter,
51+
this.rooms,
52+
new Set([...this.exceptRooms, room]),
53+
this.flags
54+
);
55+
}
56+
57+
/**
58+
* Sets the compress flag.
59+
*
60+
* @param compress - if `true`, compresses the sending data
61+
* @return a new BroadcastOperator instance
62+
* @public
63+
*/
64+
public compress(compress: boolean): BroadcastOperator {
65+
const flags = Object.assign({}, this.flags, { compress });
66+
return new BroadcastOperator(
67+
this.adapter,
68+
this.rooms,
69+
this.exceptRooms,
70+
flags
71+
);
72+
}
73+
74+
/**
75+
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
76+
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
77+
* and is in the middle of a request-response cycle).
78+
*
79+
* @return a new BroadcastOperator instance
80+
* @public
81+
*/
82+
public get volatile(): BroadcastOperator {
83+
const flags = Object.assign({}, this.flags, { volatile: true });
84+
return new BroadcastOperator(
85+
this.adapter,
86+
this.rooms,
87+
this.exceptRooms,
88+
flags
89+
);
90+
}
91+
92+
/**
93+
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
94+
*
95+
* @return a new BroadcastOperator instance
96+
* @public
97+
*/
98+
public get local(): BroadcastOperator {
99+
const flags = Object.assign({}, this.flags, { local: true });
100+
return new BroadcastOperator(
101+
this.adapter,
102+
this.rooms,
103+
this.exceptRooms,
104+
flags
105+
);
106+
}
107+
108+
/**
109+
* Emits to all clients.
110+
*
111+
* @return Always true
112+
* @public
113+
*/
114+
public emit(ev: string | Symbol, ...args: any[]): true {
115+
if (RESERVED_EVENTS.has(ev)) {
116+
throw new Error(`"${ev}" is a reserved event name`);
117+
}
118+
// set up packet object
119+
args.unshift(ev);
120+
const packet = {
121+
type: PacketType.EVENT,
122+
data: args,
123+
};
124+
125+
if ("function" == typeof args[args.length - 1]) {
126+
throw new Error("Callbacks are not supported when broadcasting");
127+
}
128+
129+
this.adapter.broadcast(packet, {
130+
rooms: this.rooms,
131+
except: this.exceptRooms,
132+
flags: this.flags,
133+
});
134+
135+
return true;
136+
}
137+
138+
/**
139+
* Gets a list of clients.
140+
*
141+
* @public
142+
*/
143+
public allSockets(): Promise<Set<SocketId>> {
144+
if (!this.adapter) {
145+
throw new Error(
146+
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
147+
);
148+
}
149+
return this.adapter.sockets(this.rooms);
150+
}
151+
}

lib/index.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import debugModule from "debug";
1616
import { Socket } from "./socket";
1717
import type { CookieSerializeOptions } from "cookie";
1818
import type { CorsOptions } from "cors";
19+
import type { BroadcastOperator } from "./broadcast-operator";
1920

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

@@ -624,25 +625,23 @@ export class Server extends EventEmitter {
624625
/**
625626
* Targets a room when emitting.
626627
*
627-
* @param name
628+
* @param room
628629
* @return self
629630
* @public
630631
*/
631-
public to(name: Room): this {
632-
this.sockets.to(name);
633-
return this;
632+
public to(room: Room): BroadcastOperator {
633+
return this.sockets.to(room);
634634
}
635635

636636
/**
637637
* Targets a room when emitting.
638638
*
639-
* @param name
639+
* @param room
640640
* @return self
641641
* @public
642642
*/
643-
public in(name: Room): this {
644-
this.sockets.in(name);
645-
return this;
643+
public in(room: Room): BroadcastOperator {
644+
return this.sockets.in(room);
646645
}
647646

648647
/**
@@ -695,9 +694,8 @@ export class Server extends EventEmitter {
695694
* @return self
696695
* @public
697696
*/
698-
public compress(compress: boolean): this {
699-
this.sockets.compress(compress);
700-
return this;
697+
public compress(compress: boolean): BroadcastOperator {
698+
return this.sockets.compress(compress);
701699
}
702700

703701
/**
@@ -708,9 +706,8 @@ export class Server extends EventEmitter {
708706
* @return self
709707
* @public
710708
*/
711-
public get volatile(): this {
712-
this.sockets.volatile;
713-
return this;
709+
public get volatile(): BroadcastOperator {
710+
return this.sockets.volatile;
714711
}
715712

716713
/**
@@ -719,9 +716,8 @@ export class Server extends EventEmitter {
719716
* @return self
720717
* @public
721718
*/
722-
public get local(): this {
723-
this.sockets.local;
724-
return this;
719+
public get local(): BroadcastOperator {
720+
return this.sockets.local;
725721
}
726722
}
727723

lib/namespace.ts

Lines changed: 19 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Socket, RESERVED_EVENTS } from "./socket";
1+
import { Socket } from "./socket";
22
import type { Server } from "./index";
33
import type { Client } from "./client";
44
import { EventEmitter } from "events";
5-
import { PacketType } from "socket.io-parser";
65
import debugModule from "debug";
76
import type { Adapter, Room, SocketId } from "socket.io-adapter";
7+
import { BroadcastOperator } from "./broadcast-operator";
88

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

@@ -26,15 +26,6 @@ export class Namespace extends EventEmitter {
2626
(socket: Socket, next: (err?: ExtendedError) => void) => void
2727
> = [];
2828

29-
/** @private */
30-
_rooms: Set<Room> = new Set();
31-
32-
/** @private */
33-
_except: Set<Room> = new Set();
34-
35-
/** @private */
36-
_flags: any = {};
37-
3829
/** @private */
3930
_ids: number = 0;
4031

@@ -105,37 +96,34 @@ export class Namespace extends EventEmitter {
10596
/**
10697
* Targets a room when emitting.
10798
*
108-
* @param name
99+
* @param room
109100
* @return self
110101
* @public
111102
*/
112-
public to(name: Room): this {
113-
this._rooms.add(name);
114-
return this;
103+
public to(room: Room): BroadcastOperator {
104+
return new BroadcastOperator(this.adapter).to(room);
115105
}
116106

117107
/**
118108
* Targets a room when emitting.
119109
*
120-
* @param name
110+
* @param room
121111
* @return self
122112
* @public
123113
*/
124-
public in(name: Room): this {
125-
this._rooms.add(name);
126-
return this;
114+
public in(room: Room): BroadcastOperator {
115+
return new BroadcastOperator(this.adapter).in(room);
127116
}
128117

129118
/**
130119
* Excludes a room when emitting.
131120
*
132-
* @param name
121+
* @param room
133122
* @return self
134123
* @public
135124
*/
136-
public except(name: Room): Namespace {
137-
this._except.add(name);
138-
return this;
125+
public except(room: Room): BroadcastOperator {
126+
return new BroadcastOperator(this.adapter).except(room);
139127
}
140128

141129
/**
@@ -202,36 +190,7 @@ export class Namespace extends EventEmitter {
202190
* @public
203191
*/
204192
public emit(ev: string | Symbol, ...args: any[]): true {
205-
if (RESERVED_EVENTS.has(ev)) {
206-
throw new Error(`"${ev}" is a reserved event name`);
207-
}
208-
// set up packet object
209-
args.unshift(ev);
210-
const packet = {
211-
type: PacketType.EVENT,
212-
data: args,
213-
};
214-
215-
if ("function" == typeof args[args.length - 1]) {
216-
throw new Error("Callbacks are not supported when broadcasting");
217-
}
218-
219-
const rooms = new Set(this._rooms);
220-
const flags = Object.assign({}, this._flags);
221-
const except = new Set(this._except);
222-
223-
// reset flags
224-
this._rooms.clear();
225-
this._flags = {};
226-
this._except.clear();
227-
228-
this.adapter.broadcast(packet, {
229-
rooms: rooms,
230-
flags: flags,
231-
except: except,
232-
});
233-
234-
return true;
193+
return new BroadcastOperator(this.adapter).emit(ev, ...args);
235194
}
236195

237196
/**
@@ -263,14 +222,7 @@ export class Namespace extends EventEmitter {
263222
* @public
264223
*/
265224
public allSockets(): Promise<Set<SocketId>> {
266-
if (!this.adapter) {
267-
throw new Error(
268-
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
269-
);
270-
}
271-
const rooms = new Set(this._rooms);
272-
this._rooms.clear();
273-
return this.adapter.sockets(rooms);
225+
return new BroadcastOperator(this.adapter).allSockets();
274226
}
275227

276228
/**
@@ -280,9 +232,8 @@ export class Namespace extends EventEmitter {
280232
* @return self
281233
* @public
282234
*/
283-
public compress(compress: boolean): this {
284-
this._flags.compress = compress;
285-
return this;
235+
public compress(compress: boolean): BroadcastOperator {
236+
return new BroadcastOperator(this.adapter).compress(compress);
286237
}
287238

288239
/**
@@ -293,9 +244,8 @@ export class Namespace extends EventEmitter {
293244
* @return self
294245
* @public
295246
*/
296-
public get volatile(): this {
297-
this._flags.volatile = true;
298-
return this;
247+
public get volatile(): BroadcastOperator {
248+
return new BroadcastOperator(this.adapter).volatile;
299249
}
300250

301251
/**
@@ -304,8 +254,7 @@ export class Namespace extends EventEmitter {
304254
* @return self
305255
* @public
306256
*/
307-
public get local(): this {
308-
this._flags.local = true;
309-
return this;
257+
public get local(): BroadcastOperator {
258+
return new BroadcastOperator(this.adapter).local;
310259
}
311260
}

0 commit comments

Comments
 (0)