-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathadapter.test.ts
More file actions
165 lines (137 loc) Β· 5.9 KB
/
Copy pathadapter.test.ts
File metadata and controls
165 lines (137 loc) Β· 5.9 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
import { expect, it } from 'vitest';
import { Adapter, Server } from './index';
import { receive, track } from './test-events';
// These tests exercise smocket's own `io.adapter` seam, so they import the Server
// directly rather than through the dual-run `setupServer`: real socket.io has no
// signature-compatible counterpart (see docs/differences.md Β§B), so there is
// nothing to compare against. They run identically under both `pnpm test` targets.
/**
* A trace adapter: it records the target set of every routing query and otherwise
* behaves exactly like the built-in one. This is the "observe the target set"
* use, and it is the shape a custom adapter takes: extend `Adapter`, override
* `socketsIn`, reuse the inherited membership bookkeeping.
*/
class SpyAdapter extends Adapter {
readonly targeted: Array<Set<string>> = [];
override socketsIn(rooms: Iterable<string>): Set<string> {
const result = super.socketsIn(rooms);
if (result.size > 0) this.targeted.push(new Set(result));
return result;
}
}
/**
* A drop adapter: it removes one chosen sid from the target set, routing that
* socket out of every broadcast. This is the "narrow the target set" use, pure
* targeting with no effect on delivery order.
*/
class DropAdapter extends Adapter {
block: string | undefined;
override socketsIn(rooms: Iterable<string>): Set<string> {
const result = super.socketsIn(rooms);
if (this.block !== undefined) result.delete(this.block);
return result;
}
}
class StaleSidAdapter extends Adapter {
override socketsIn(rooms: Iterable<string>): Set<string> {
const names = [...rooms];
const result = super.socketsIn(names);
if (names.length > 0) result.add('stale-sid');
return result;
}
}
it('Adapter ignores deletion for membership it does not hold', () => {
const adapter = new Adapter();
expect(() => adapter.del('missing-sid', 'missing-room')).not.toThrow();
expect(adapter.rooms.size).toBe(0);
expect(adapter.sids.size).toBe(0);
});
it('broadcast routing ignores stale sids returned by a custom adapter', async () => {
const server = new Server('http://localhost');
server.adapter(() => new StaleSidAdapter());
const client = server.connect();
const socket = await server.nextConnection();
await socket.join('room');
const received = receive(client, 'msg');
expect(() => server.to('room').emit('msg', 'hello')).not.toThrow();
await expect(received).resolves.toBe('hello');
await server.close();
});
it('io.adapter registers a custom adapter that observes the routing decision', async () => {
const server = new Server('http://localhost');
const spy = new SpyAdapter();
// The factory receives the namespace; this example does not need it. One
// namespace is used here, so returning a single prebuilt instance is enough.
server.adapter(() => spy);
const client1 = server.connect();
const socket1 = await server.nextConnection();
server.connect();
const socket2 = await server.nextConnection();
await socket1.join('room');
const got1 = receive(client1, 'msg');
server.to('room').emit('msg', 'hello');
await expect(got1).resolves.toBe('hello');
// The spy saw the room's single member as the broadcast's target set, and never
// the socket outside the room.
expect(spy.targeted.some((set) => set.size === 1 && set.has(socket1.id))).toBe(true);
expect(spy.targeted.every((set) => !set.has(socket2.id))).toBe(true);
});
it('a custom adapter can drop a socket from the target set, and per-socket order still holds', async () => {
const server = new Server('http://localhost');
const drop = new DropAdapter();
server.adapter(() => drop);
const client1 = server.connect();
const socket1 = await server.nextConnection();
const client2 = server.connect();
const socket2 = await server.nextConnection();
await socket1.join('all');
await socket2.join('all');
drop.block = socket2.id;
const got1 = receive(client1, 'msg');
const missed2 = track(client2, 'msg');
const marker2 = receive(client2, 'marker');
server.to('all').emit('msg', 'hello');
socket2.emit('marker');
await expect(got1).resolves.toBe('hello');
await marker2;
// socket2 was routed out of the broadcast. The marker, sent later on the same
// socket, arriving proves the msg was never coming: the drop is real, and it
// relies on per-socket FIFO (0010), which the custom adapter did not disturb.
expect(missed2.received).toBe(false);
});
it('registering a custom adapter preserves per-socket delivery order', async () => {
const server = new Server('http://localhost');
server.adapter(() => new SpyAdapter());
const client1 = server.connect();
const socket1 = await server.nextConnection();
await socket1.join('all');
const order: string[] = [];
client1.on('a', () => order.push('a'));
client1.on('b', () => order.push('b'));
const done = receive(client1, 'marker');
server.to('all').emit('a');
server.to('all').emit('b');
socket1.emit('marker');
await done;
// Two broadcasts routed through the custom adapter, then a direct marker: the
// socket still observes them in send order, so the adapter changed targeting
// without touching the delivery path.
expect(order).toEqual(['a', 'b']);
});
it('builds an independent registered adapter for each dynamic concrete child', async () => {
const server = new Server('http://localhost');
const adapters = new Map<string, SpyAdapter>();
server.adapter((namespace) => {
const adapter = new SpyAdapter();
adapters.set(namespace.name, adapter);
return adapter;
});
server.of(/^\/tenant-/);
const a = server.connect('/tenant-a');
const b = server.connect('/tenant-b');
await Promise.all([receive(a, 'connect'), receive(b, 'connect')]);
expect(adapters.get('/tenant-a')).toBe(server.of('/tenant-a').adapter);
expect(adapters.get('/tenant-b')).toBe(server.of('/tenant-b').adapter);
expect(adapters.get('/tenant-a')).not.toBe(adapters.get('/tenant-b'));
await server.close();
});