-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathvolatile.test.ts
More file actions
366 lines (304 loc) Β· 14.6 KB
/
Copy pathvolatile.test.ts
File metadata and controls
366 lines (304 loc) Β· 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import { expect, it } from 'vitest';
import type { ServerSocketContract } from './contract';
import { setupServer } from './setup-server';
import { observeDisconnect, receive, track } from './test-events';
const ctx = setupServer();
// 0016: `volatile` is a plain emit while the socket is connected and is dropped only when it
// is sent in the pre-connect window (0004). The steady-state cases prove the "plain emit"
// half; the two drop cases prove the window, both proven with the marker pattern rather than
// a timeout (a later normal event is shown to arrive while the volatile one never did).
it('a volatile emit is delivered on a connected socket (server to client)', async () => {
const { client, serverSocket } = await ctx.connectClient();
const got = receive(client, 'vol');
const volatile = serverSocket.volatile;
expect(volatile).toBe(serverSocket);
volatile.emit('vol', 'hi');
await expect(got).resolves.toBe('hi');
});
it('a volatile emit is delivered on a connected socket (client to server)', async () => {
// `.volatile` is meaningful in both directions; on a connected socket it is a plain emit.
const { client, serverSocket } = await ctx.connectClient();
const got = new Promise((resolve) => serverSocket.on('vol', resolve));
const volatile = client.volatile;
expect(volatile).toBe(client);
volatile.emit('vol', 'hi');
await expect(got).resolves.toBe('hi');
});
it('io.volatile.to(room) routes to the room like a normal broadcast in steady state', async () => {
const { client: client1, serverSocket: socket1 } = await ctx.connectClient();
const { client: client2, serverSocket: socket2 } = await ctx.connectClient();
await socket1.join('room');
const got1 = receive(client1, 'msg');
const msg2 = track(client2, 'msg');
const marker2 = receive(client2, 'marker');
ctx.io.volatile.to('room').emit('msg', 'hello');
socket2.emit('marker');
await expect(got1).resolves.toBe('hello');
await marker2;
expect(msg2.received).toBe(false); // outside the room
});
it('socket.volatile.broadcast reaches everyone except the sender in steady state', async () => {
const { client: client1, serverSocket: socket1 } = await ctx.connectClient();
const { client: client2 } = await ctx.connectClient();
const got2 = receive(client2, 'msg');
const msg1 = track(client1, 'msg');
const marker1 = receive(client1, 'marker');
socket1.volatile.broadcast.emit('msg', 'hello');
socket1.emit('marker');
await expect(got2).resolves.toBe('hello');
await marker1;
expect(msg1.received).toBe(false); // sender excluded
});
it('io.to(room).volatile and io.volatile.to(room) preserve the same target', async () => {
const { client: member, serverSocket: memberSocket } = await ctx.connectClient();
const { client: outside, serverSocket: outsideSocket } = await ctx.connectClient();
await memberSocket.join('room');
const narrowFirst = receive(member, 'narrow-first');
const volatileFirst = receive(member, 'volatile-first');
const outsideNarrow = track(outside, 'narrow-first');
const outsideVolatile = track(outside, 'volatile-first');
const marker = receive(outside, 'marker');
ctx.io.to('room').volatile.emit('narrow-first', 'hello');
await expect(narrowFirst).resolves.toBe('hello');
ctx.io.volatile.to('room').emit('volatile-first', 'hello');
await expect(volatileFirst).resolves.toBe('hello');
outsideSocket.emit('marker');
await marker;
expect(outsideNarrow.received).toBe(false);
expect(outsideVolatile.received).toBe(false);
});
it('namespace narrowing and volatile preserve each other in either order', async () => {
const { client: member, serverSocket: memberSocket } = await ctx.connectClient({
namespace: '/game',
});
const { client: outside, serverSocket: outsideSocket } = await ctx.connectClient({
namespace: '/game',
});
await memberSocket.join('room');
const narrowFirst = receive(member, 'narrow-first');
const volatileFirst = receive(member, 'volatile-first');
const outsideNarrow = track(outside, 'narrow-first');
const outsideVolatile = track(outside, 'volatile-first');
const marker = receive(outside, 'marker');
const game = ctx.io.of('/game');
game.to('room').volatile.emit('narrow-first', 'hello');
await expect(narrowFirst).resolves.toBe('hello');
game.volatile.to('room').emit('volatile-first', 'hello');
await expect(volatileFirst).resolves.toBe('hello');
outsideSocket.emit('marker');
await marker;
expect(outsideNarrow.received).toBe(false);
expect(outsideVolatile.received).toBe(false);
});
it('socket.to(room).volatile and socket.volatile.to(room) keep sender exclusion', async () => {
const { client: sender, serverSocket: senderSocket } = await ctx.connectClient();
const { client: recipient, serverSocket: recipientSocket } = await ctx.connectClient();
await senderSocket.join('room');
await recipientSocket.join('room');
const narrowFirst = receive(recipient, 'narrow-first');
const volatileFirst = receive(recipient, 'volatile-first');
const senderNarrow = track(sender, 'narrow-first');
const senderVolatile = track(sender, 'volatile-first');
const marker = receive(sender, 'marker');
senderSocket.to('room').volatile.emit('narrow-first', 'hello');
await expect(narrowFirst).resolves.toBe('hello');
senderSocket.volatile.to('room').emit('volatile-first', 'hello');
await expect(volatileFirst).resolves.toBe('hello');
senderSocket.emit('marker');
await marker;
expect(senderNarrow.received).toBe(false);
expect(senderVolatile.received).toBe(false);
});
it('socket.broadcast.volatile and socket.volatile.broadcast keep sender exclusion', async () => {
const { client: sender, serverSocket: senderSocket } = await ctx.connectClient();
const { client: recipient } = await ctx.connectClient();
const narrowFirst = receive(recipient, 'narrow-first');
const volatileFirst = receive(recipient, 'volatile-first');
const senderNarrow = track(sender, 'narrow-first');
const senderVolatile = track(sender, 'volatile-first');
const marker = receive(sender, 'marker');
senderSocket.broadcast.volatile.emit('narrow-first', 'hello');
await expect(narrowFirst).resolves.toBe('hello');
senderSocket.volatile.broadcast.emit('volatile-first', 'hello');
await expect(volatileFirst).resolves.toBe('hello');
senderSocket.emit('marker');
await marker;
expect(senderNarrow.received).toBe(false);
expect(senderVolatile.received).toBe(false);
});
it('volatile stays immutable and survives to, in, except, and timeout in either order', async () => {
const { client, serverSocket } = await ctx.connectClient();
const { disconnected } = observeDisconnect(serverSocket);
client.disconnect();
await disconnected;
const plain = receive(client, 'plain');
const first = track(client, 'volatile-last');
const second = track(client, 'volatile-first');
const marker = receive(client, 'marker');
ctx.io.on('connection', (socket: ServerSocketContract) => {
const base = ctx.io.to(socket.id);
const volatile = base.volatile;
expect(volatile).not.toBe(base);
expect(base.volatile).not.toBe(volatile);
base.emit('plain', 'kept');
base.in(socket.id).except('nobody').timeout(100).volatile.emit('volatile-last', 'dropped');
volatile.in(socket.id).except('nobody').timeout(100).emit('volatile-first', 'dropped');
socket.emit('marker', 'done');
});
client.connect();
await expect(plain).resolves.toBe('kept');
await expect(marker).resolves.toBe('done');
expect(first.received).toBe(false);
expect(second.received).toBe(false);
});
it('a volatile emit still carries an ack, which round-trips when delivered', async () => {
const { client, serverSocket } = await ctx.connectClient();
client.on('q', (n: number, ack: (r: number) => void) => ack(n * 2));
const answer = await new Promise((resolve) => {
serverSocket.volatile.emit('q', 5, resolve);
});
expect(answer).toBe(10);
});
it('volatile emitWithAck delivers and fires outgoing catch-alls in both directions', async () => {
const { client, serverSocket } = await ctx.connectClient();
const serverOutgoing: string[] = [];
const clientOutgoing: string[] = [];
serverSocket.onAnyOutgoing((event) => serverOutgoing.push(String(event)));
client.onAnyOutgoing((event) => clientOutgoing.push(String(event)));
client.on('server-q', (n: number, ack: (r: number) => void) => ack(n * 2));
serverSocket.on('client-q', (n: number, ack: (r: number) => void) => ack(n + 3));
await expect(serverSocket.volatile.emitWithAck('server-q', 5)).resolves.toBe(10);
await expect(client.volatile.emitWithAck('client-q', 7)).resolves.toBe(10);
expect(serverOutgoing).toEqual(['server-q']);
expect(clientOutgoing).toEqual(['client-q']);
});
it('a volatile emit to a recipient still in the pre-connect window is dropped', async () => {
// Inside the connection handler the paired client has not yet completed its connection
// (0004), so a volatile emit to it is dropped while a normal one is delivered. Reached
// through a reconnect so the client and its listeners already exist; the marker proves
// non-receipt without a timeout.
const { client, serverSocket } = await ctx.connectClient();
const { disconnected } = observeDisconnect(serverSocket);
client.disconnect();
await disconnected;
const vol = track(client, 'vol');
const volAck = track(client, 'vol-ack');
const marker = receive(client, 'marker');
ctx.io.on('connection', (socket: ServerSocketContract) => {
expect(socket.connected).toBe(true);
expect(socket.disconnected).toBe(false);
const volatile = socket.volatile;
expect(volatile).toBe(socket);
volatile.emit('vol', 'dropped'); // pre-connect: dropped
void socket.volatile.emitWithAck('vol-ack'); // pre-connect: dropped, ack stays pending
socket.emit('marker', 'ok'); // pre-connect: buffered, then delivered
});
client.connect();
await expect(marker).resolves.toBe('ok');
expect(vol.received).toBe(false);
expect(volAck.received).toBe(false);
});
it('a volatile emit from a client still in the pre-connect window is dropped', async () => {
// The mirror of the case above, from the client's side of the same window. `openClient`
// returns before the connection completes (0004), so an emit issued right after it is in
// the window: the volatile one is dropped and the plain one is buffered and replayed on
// connect. The plain emit is the marker. Had the volatile one been buffered instead of
// dropped it would sit ahead of the marker in the same queue and arrive first, so what the
// server ends up seeing is the proof and no timeout is involved.
const seen: string[] = [];
const delivered = new Promise<void>((resolve) => {
ctx.io.on('connection', (socket: ServerSocketContract) => {
socket.on('vol', (value: string) => seen.push(`vol:${value}`));
socket.on('vol-ack', () => seen.push('vol-ack'));
socket.on('marker', (value: string) => {
seen.push(`marker:${value}`);
resolve();
});
});
});
const client = ctx.openClient();
expect(client.connected).toBe(false);
const volatile = client.volatile;
expect(volatile).toBe(client);
volatile.emit('vol', 'dropped'); // pre-connect: dropped
// Real socket.io-client rejects this pending ack during fixture teardown.
void client.volatile.emitWithAck('vol-ack').catch(() => undefined); // pre-connect: dropped
client.emit('marker', 'ok'); // pre-connect: buffered, then delivered
await delivered;
expect(seen).toEqual(['marker:ok']);
});
it('consumes volatile once when the same server socket reference is reused', async () => {
const { client, serverSocket } = await ctx.connectClient();
const { disconnected } = observeDisconnect(serverSocket);
client.disconnect();
await disconnected;
const dropped = track(client, 'dropped');
const delivered = receive(client, 'delivered');
ctx.io.on('connection', (socket: ServerSocketContract) => {
const held = socket.volatile;
held.emit('dropped');
held.emit('delivered', 'plain');
});
client.connect();
await expect(delivered).resolves.toBe('plain');
expect(dropped.received).toBe(false);
});
it('keeps a recipient volatile flag pending across an unrelated broadcast', async () => {
const { client, serverSocket } = await ctx.connectClient();
client.on('broadcast-ack', (ack: (value: string) => void) => ack('broadcast'));
const { disconnected } = observeDisconnect(serverSocket);
client.disconnect();
await disconnected;
const broadcast = receive(client, 'broadcast');
const dropped = track(client, 'dropped');
const marker = receive(client, 'marker');
const collected = new Promise<unknown[]>((resolve) => {
ctx.io.on('connection', (socket: ServerSocketContract) => {
expect(socket.volatile).toBe(socket);
ctx.io.emit('broadcast', 'kept');
ctx.io.timeout(1000).emit('broadcast-ack', (...args: unknown[]) => resolve(args));
socket.emit('dropped', 'not-kept');
socket.emit('marker', 'done');
});
});
client.connect();
await expect(broadcast).resolves.toBe('kept');
await expect(collected).resolves.toEqual([null, ['broadcast']]);
await expect(marker).resolves.toBe('done');
expect(dropped.received).toBe(false);
});
it.each(['to', 'except', 'broadcast'] as const)(
'transfers a server volatile flag once to the %s operator',
async (entry) => {
const { client: sender, serverSocket: senderSocket } = await ctx.connectClient();
const { client: recipient, serverSocket: recipientSocket } = await ctx.connectClient();
const { disconnected } = observeDisconnect(recipientSocket);
recipient.disconnect();
await disconnected;
const dropped = track(recipient, 'dropped');
const marker = receive(recipient, 'marker');
const direct = receive(sender, 'direct');
ctx.io.on('connection', (socket: ServerSocketContract) => {
expect(senderSocket.volatile).toBe(senderSocket);
const operator =
entry === 'to'
? senderSocket.to(socket.id)
: entry === 'except'
? senderSocket.except('nobody')
: senderSocket.broadcast;
operator.emit('dropped', entry);
senderSocket.emit('direct', 'plain');
socket.emit('marker', 'done');
});
recipient.connect();
await expect(direct).resolves.toBe('plain');
await expect(marker).resolves.toBe('done');
expect(dropped.received).toBe(false);
},
);
// The reconnect window is the one left unasserted, and it is not the window above. A
// volatile emit on a client that connected and then disconnected is buffered by real
// socket.io-client and replayed on reconnect, which is reconnection behaviour and outside
// smocket's scope (docs/scope.md), so a dual-run case there would compare two different
// things. Before the first connection completes both targets drop, which is why that half
// is asserted.