onelobby 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/dist/chat.d.ts +60 -0
  4. package/dist/chat.js +163 -0
  5. package/dist/chat.js.map +1 -0
  6. package/dist/chunking.d.ts +17 -0
  7. package/dist/chunking.js +59 -0
  8. package/dist/chunking.js.map +1 -0
  9. package/dist/clock.d.ts +8 -0
  10. package/dist/clock.js +39 -0
  11. package/dist/clock.js.map +1 -0
  12. package/dist/codec.d.ts +141 -0
  13. package/dist/codec.js +308 -0
  14. package/dist/codec.js.map +1 -0
  15. package/dist/crockford.d.ts +14 -0
  16. package/dist/crockford.js +55 -0
  17. package/dist/crockford.js.map +1 -0
  18. package/dist/deflate.d.ts +4 -0
  19. package/dist/deflate.js +32 -0
  20. package/dist/deflate.js.map +1 -0
  21. package/dist/deprecated.d.ts +18 -0
  22. package/dist/deprecated.js +11 -0
  23. package/dist/deprecated.js.map +1 -0
  24. package/dist/direct/hub-host.d.ts +82 -0
  25. package/dist/direct/hub-host.js +393 -0
  26. package/dist/direct/hub-host.js.map +1 -0
  27. package/dist/direct/hub.d.ts +178 -0
  28. package/dist/direct/hub.js +862 -0
  29. package/dist/direct/hub.js.map +1 -0
  30. package/dist/direct/index.d.ts +57 -0
  31. package/dist/direct/index.js +71 -0
  32. package/dist/direct/index.js.map +1 -0
  33. package/dist/direct/portmap.d.ts +84 -0
  34. package/dist/direct/portmap.js +465 -0
  35. package/dist/direct/portmap.js.map +1 -0
  36. package/dist/direct-address.d.ts +20 -0
  37. package/dist/direct-address.js +61 -0
  38. package/dist/direct-address.js.map +1 -0
  39. package/dist/emitter.d.ts +9 -0
  40. package/dist/emitter.js +36 -0
  41. package/dist/emitter.js.map +1 -0
  42. package/dist/friendly.d.ts +20 -0
  43. package/dist/friendly.js +120 -0
  44. package/dist/friendly.js.map +1 -0
  45. package/dist/global.d.ts +8 -0
  46. package/dist/global.js +9 -0
  47. package/dist/global.js.map +1 -0
  48. package/dist/gridpaint.d.ts +27 -0
  49. package/dist/gridpaint.js +106 -0
  50. package/dist/gridpaint.js.map +1 -0
  51. package/dist/hub.d.ts +25 -0
  52. package/dist/hub.js +2 -0
  53. package/dist/hub.js.map +1 -0
  54. package/dist/index.d.ts +35 -0
  55. package/dist/index.js +25 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/internals.d.ts +28 -0
  58. package/dist/internals.js +24 -0
  59. package/dist/internals.js.map +1 -0
  60. package/dist/joincode.d.ts +21 -0
  61. package/dist/joincode.js +84 -0
  62. package/dist/joincode.js.map +1 -0
  63. package/dist/lobby.d.ts +203 -0
  64. package/dist/lobby.js +840 -0
  65. package/dist/lobby.js.map +1 -0
  66. package/dist/lockstep.d.ts +132 -0
  67. package/dist/lockstep.js +471 -0
  68. package/dist/lockstep.js.map +1 -0
  69. package/dist/migration.d.ts +17 -0
  70. package/dist/migration.js +34 -0
  71. package/dist/migration.js.map +1 -0
  72. package/dist/onelobby.bundle.mjs +140 -0
  73. package/dist/onelobby.bundle.mjs.map +7 -0
  74. package/dist/onelobby.d.ts +151 -0
  75. package/dist/onelobby.global.js +3897 -0
  76. package/dist/onelobby.global.js.map +7 -0
  77. package/dist/onelobby.global.min.js +140 -0
  78. package/dist/onelobby.global.min.js.map +7 -0
  79. package/dist/onelobby.js +337 -0
  80. package/dist/onelobby.js.map +1 -0
  81. package/dist/signaling.d.ts +94 -0
  82. package/dist/signaling.js +134 -0
  83. package/dist/signaling.js.map +1 -0
  84. package/dist/socket.d.ts +63 -0
  85. package/dist/socket.js +142 -0
  86. package/dist/socket.js.map +1 -0
  87. package/dist/testing.d.ts +135 -0
  88. package/dist/testing.js +438 -0
  89. package/dist/testing.js.map +1 -0
  90. package/dist/transport.d.ts +80 -0
  91. package/dist/transport.js +7 -0
  92. package/dist/transport.js.map +1 -0
  93. package/dist/ui/controller.d.ts +137 -0
  94. package/dist/ui/controller.js +337 -0
  95. package/dist/ui/controller.js.map +1 -0
  96. package/dist/ui/element.d.ts +22 -0
  97. package/dist/ui/element.js +284 -0
  98. package/dist/ui/element.js.map +1 -0
  99. package/dist/ui/index.d.ts +5 -0
  100. package/dist/ui/index.js +9 -0
  101. package/dist/ui/index.js.map +1 -0
  102. package/dist/ui/server-url.d.ts +9 -0
  103. package/dist/ui/server-url.js +39 -0
  104. package/dist/ui/server-url.js.map +1 -0
  105. package/dist/ui/styles.d.ts +2 -0
  106. package/dist/ui/styles.js +70 -0
  107. package/dist/ui/styles.js.map +1 -0
  108. package/dist/voice-browser.d.ts +19 -0
  109. package/dist/voice-browser.js +151 -0
  110. package/dist/voice-browser.js.map +1 -0
  111. package/dist/voice-core.d.ts +98 -0
  112. package/dist/voice-core.js +233 -0
  113. package/dist/voice-core.js.map +1 -0
  114. package/dist/voice-types.d.ts +35 -0
  115. package/dist/voice-types.js +3 -0
  116. package/dist/voice-types.js.map +1 -0
  117. package/dist/voice.d.ts +68 -0
  118. package/dist/voice.js +223 -0
  119. package/dist/voice.js.map +1 -0
  120. package/dist/webrtc.d.ts +29 -0
  121. package/dist/webrtc.js +169 -0
  122. package/dist/webrtc.js.map +1 -0
  123. package/package.json +93 -4
@@ -0,0 +1,438 @@
1
+ import { CROCKFORD_ALPHABET, normalizeCode } from "./crockford.js";
2
+ /** Deterministic clock: time only moves through `advance()`. Timers fire in (time, creation) order. */
3
+ export class ManualClock {
4
+ t = 0;
5
+ seq = 0;
6
+ timers = [];
7
+ now() {
8
+ return this.t;
9
+ }
10
+ setInterval(fn, ms) {
11
+ const timer = { at: this.t + ms, seq: this.seq++, fn, every: Math.max(1, ms) };
12
+ this.timers.push(timer);
13
+ return () => {
14
+ timer.dead = true;
15
+ };
16
+ }
17
+ setTimeout(fn, ms) {
18
+ const timer = { at: this.t + ms, seq: this.seq++, fn };
19
+ this.timers.push(timer);
20
+ return () => {
21
+ timer.dead = true;
22
+ };
23
+ }
24
+ /** Advance time, firing due timers one at a time (microtasks flush between them when awaited via `run`). */
25
+ advance(ms) {
26
+ const end = this.t + ms;
27
+ for (;;) {
28
+ this.timers = this.timers.filter((x) => !x.dead);
29
+ let next;
30
+ for (const x of this.timers)
31
+ if (x.at <= end && (!next || x.at < next.at || (x.at === next.at && x.seq < next.seq)))
32
+ next = x;
33
+ if (!next)
34
+ break;
35
+ this.t = Math.max(this.t, next.at);
36
+ if (next.every) {
37
+ next.at += next.every;
38
+ next.seq = this.seq++;
39
+ }
40
+ else {
41
+ next.dead = true;
42
+ }
43
+ next.fn();
44
+ }
45
+ this.t = end;
46
+ }
47
+ /** Advance in `step` ms slices, letting promises and queued microtasks settle between slices. */
48
+ async run(ms, step = 10) {
49
+ let left = ms;
50
+ await settle();
51
+ while (left > 0) {
52
+ const d = Math.min(step, left);
53
+ this.advance(d);
54
+ left -= d;
55
+ await settle();
56
+ }
57
+ }
58
+ }
59
+ /** Let all pending microtasks (and chained promise continuations) run. */
60
+ export async function settle(rounds = 8) {
61
+ for (let i = 0; i < rounds; i++)
62
+ await Promise.resolve();
63
+ // A macrotask boundary guarantees the whole microtask queue (delivery chains) has drained.
64
+ await new Promise((r) => (globalThis.setImmediate?.(r) ?? setTimeout(r, 0)));
65
+ }
66
+ // ---- in-memory peer network ---------------------------------------------------------------------------------------
67
+ let linkSerial = 0;
68
+ /**
69
+ * Pairs `PeerLink`s through the signaling path like WebRTC would: the offerer's SDP string names it, the answerer
70
+ * resolves that name, and both open one microtask later. Messages are delivered FIFO, one microtask later.
71
+ */
72
+ export class MemoryNetwork {
73
+ offers = new Map();
74
+ /** Every link ever created (for assertions and fault injection). */
75
+ links = [];
76
+ /** Set to drop all traffic in one direction pair for fault injection: key `${from}->${to}` is not tracked; use link.mute(). */
77
+ factory = { create: (opts) => new MemoryLink(this, opts) };
78
+ /** @internal */
79
+ register(offer, link) {
80
+ this.offers.set(offer, link);
81
+ this.links.push(link);
82
+ }
83
+ /** @internal */
84
+ lookup(offer) {
85
+ return this.offers.get(offer);
86
+ }
87
+ /** Kill every link whose far end is this identity-tagged factory owner (set `tag` via MemoryLink.owner). */
88
+ killOwner(owner) {
89
+ for (const l of this.links)
90
+ if (l.owner === owner)
91
+ l.close();
92
+ }
93
+ /** Per-owner factory so tests can tell whose link is whose and cut one peer off. */
94
+ factoryFor(owner) {
95
+ return {
96
+ create: (opts) => {
97
+ const l = new MemoryLink(this, opts);
98
+ l.owner = owner;
99
+ return l;
100
+ },
101
+ };
102
+ }
103
+ }
104
+ export class MemoryLink {
105
+ net;
106
+ remoteId;
107
+ isOpen = false;
108
+ owner = "";
109
+ onSignal = null;
110
+ onOpen = null;
111
+ onClose = null;
112
+ onMessage = null;
113
+ onPath = null;
114
+ /** Bytes sent per channel, for assertions. */
115
+ sent = { reliable: 0, fast: 0, voice: 0 };
116
+ /** While true, outgoing messages vanish (a frozen or partitioned peer). */
117
+ blackhole = false;
118
+ peer = null;
119
+ closed = false;
120
+ id = `mem-${++linkSerial}`;
121
+ constructor(net, opts) {
122
+ this.net = net;
123
+ this.remoteId = opts.remoteId;
124
+ }
125
+ startOffer() {
126
+ this.net.register(this.id, this);
127
+ queueMicrotask(() => this.onSignal?.({ sdp: { type: "offer", sdp: this.id } }));
128
+ }
129
+ handleSignal(s) {
130
+ if (s.sdp?.type === "offer") {
131
+ const offerer = this.net.lookup(s.sdp.sdp);
132
+ this.net.links.push(this);
133
+ if (!offerer)
134
+ return;
135
+ this.peer = offerer;
136
+ queueMicrotask(() => this.onSignal?.({ sdp: { type: "answer", sdp: this.id } }));
137
+ queueMicrotask(() => offerer.accept(this));
138
+ queueMicrotask(() => this.open());
139
+ }
140
+ }
141
+ /** @internal */
142
+ accept(answerer) {
143
+ this.peer = answerer;
144
+ this.open();
145
+ }
146
+ open() {
147
+ if (this.closed || this.isOpen)
148
+ return;
149
+ this.isOpen = true;
150
+ this.onOpen?.();
151
+ this.onPath?.("direct");
152
+ }
153
+ send(channel, bytes) {
154
+ if (!this.isOpen || this.closed)
155
+ return;
156
+ this.sent[channel] += bytes.length;
157
+ if (this.blackhole)
158
+ return;
159
+ const peer = this.peer;
160
+ const copy = bytes.slice();
161
+ queueMicrotask(() => {
162
+ if (peer && peer.isOpen && !peer.closed && !this.closed)
163
+ peer.onMessage?.(channel, copy);
164
+ });
165
+ }
166
+ close() {
167
+ if (this.closed)
168
+ return;
169
+ this.closed = true;
170
+ this.isOpen = false;
171
+ const peer = this.peer;
172
+ queueMicrotask(() => {
173
+ this.onClose?.("closed");
174
+ if (peer && !peer.closed)
175
+ peer.closeFromRemote();
176
+ });
177
+ }
178
+ closeFromRemote() {
179
+ if (this.closed)
180
+ return;
181
+ this.closed = true;
182
+ this.isOpen = false;
183
+ this.onClose?.("remote closed");
184
+ }
185
+ }
186
+ /** Minimal onelobby-server (lk1) for client tests. Not a conformance target: stream A owns the real server. */
187
+ export class MemoryServer {
188
+ lobbies = new Map();
189
+ /** Every socket ever opened (to drop one from a test). */
190
+ sockets = [];
191
+ socketFactory = () => this.connect();
192
+ n = 0;
193
+ iceServers = [{ urls: ["stun:stun.example:3478"] }];
194
+ /** Log of every client message, for assertions. */
195
+ received = [];
196
+ connect() {
197
+ const s = new MemorySocket(this);
198
+ this.sockets.push(s);
199
+ return s;
200
+ }
201
+ /** @internal */
202
+ newCode() {
203
+ let x = 0x9e3779b1 * (++this.n + 17);
204
+ let s = "";
205
+ for (let i = 0; i < 6; i++) {
206
+ x = (Math.imul(x, 1103515245) + 12345) >>> 0;
207
+ s += CROCKFORD_ALPHABET[(x >>> 16) & 31];
208
+ }
209
+ return this.lobbies.has(s) ? this.newCode() : s;
210
+ }
211
+ }
212
+ export class MemorySocket {
213
+ srv;
214
+ readyState = 0;
215
+ onopen = null;
216
+ onmessage = null;
217
+ onclose = null;
218
+ onerror = null;
219
+ hello = null;
220
+ lobby = null;
221
+ peer = null;
222
+ closed = false;
223
+ constructor(srv) {
224
+ this.srv = srv;
225
+ queueMicrotask(() => {
226
+ this.readyState = 1;
227
+ this.onopen?.();
228
+ });
229
+ }
230
+ // ---- socket API
231
+ send(data) {
232
+ if (this.closed)
233
+ return;
234
+ const m = JSON.parse(data);
235
+ queueMicrotask(() => this.handle(m));
236
+ }
237
+ close() {
238
+ if (this.closed)
239
+ return;
240
+ this.closed = true;
241
+ this.readyState = 3;
242
+ this.leaveLobby("timeout");
243
+ queueMicrotask(() => this.onclose?.());
244
+ }
245
+ /** @internal server -> this client */
246
+ push(m) {
247
+ if (this.closed)
248
+ return;
249
+ queueMicrotask(() => this.onmessage?.({ data: JSON.stringify(m) }));
250
+ }
251
+ err(code, msg = "", data) {
252
+ this.push({ t: "error", code, msg, ...(data ? { data } : {}) });
253
+ }
254
+ lobbyMsg(l, p) {
255
+ return {
256
+ t: "lobby",
257
+ code: l.code,
258
+ gameId: l.gameId,
259
+ netVersion: l.netVersion,
260
+ hostId: l.hostId,
261
+ you: p.peerId,
262
+ mode: l.mode,
263
+ tickRate: l.tickRate,
264
+ lateJoin: l.lateJoin,
265
+ phase: l.phase,
266
+ meta: l.meta,
267
+ peers: [...l.peers.values()].map((q) => ({ peerId: q.peerId, name: q.name, joinedAt: q.joinedAt })),
268
+ resumeToken: p.token,
269
+ name: l.name,
270
+ maxPlayers: l.maxPlayers,
271
+ visibility: l.visibility,
272
+ };
273
+ }
274
+ broadcast(l, m, except) {
275
+ for (const q of l.peers.values())
276
+ if (q.peerId !== except)
277
+ q.sock?.push(m);
278
+ }
279
+ leaveLobby(reason) {
280
+ const l = this.lobby;
281
+ const p = this.peer;
282
+ if (!l || !p)
283
+ return;
284
+ this.lobby = null;
285
+ this.peer = null;
286
+ p.sock = null;
287
+ l.peers.delete(p.peerId);
288
+ if (reason === "timeout")
289
+ l.reserved.set(p.token, p); // slot kept for resume
290
+ if (l.peers.size === 0) {
291
+ this.srv.lobbies.delete(l.code);
292
+ return;
293
+ }
294
+ this.broadcast(l, { t: "peer_left", peerId: p.peerId, reason });
295
+ if (l.hostId === p.peerId) {
296
+ l.hostId = Math.min(...l.peers.keys());
297
+ l.epoch++;
298
+ this.broadcast(l, { t: "host_changed", hostId: l.hostId, epoch: l.epoch });
299
+ }
300
+ }
301
+ handle(m) {
302
+ if (this.closed)
303
+ return;
304
+ this.srv.received.push(m);
305
+ const t = String(m.t);
306
+ if (t === "ping")
307
+ return this.push({ t: "pong", n: m.n });
308
+ if (t === "hello") {
309
+ if (this.hello)
310
+ return this.err("bad_request", "second hello");
311
+ this.hello = m;
312
+ const tok = typeof m.resumeToken === "string" ? m.resumeToken : "";
313
+ for (const l of this.srv.lobbies.values()) {
314
+ const rp = l.reserved.get(tok);
315
+ if (!rp)
316
+ continue;
317
+ l.reserved.delete(tok);
318
+ rp.sock = this;
319
+ rp.token = tok + "+";
320
+ l.peers.set(rp.peerId, rp);
321
+ this.lobby = l;
322
+ this.peer = rp;
323
+ this.push({ t: "welcome", peerId: rp.peerId, ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });
324
+ this.push(this.lobbyMsg(l, rp));
325
+ return this.broadcast(l, { t: "peer_joined", peer: { peerId: rp.peerId, name: rp.name, joinedAt: rp.joinedAt } }, rp.peerId);
326
+ }
327
+ return this.push({ t: "welcome", ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });
328
+ }
329
+ if (!this.hello)
330
+ return this.err("bad_request", "hello first");
331
+ const gameId = String(this.hello.gameId);
332
+ const netVersion = String(this.hello.netVersion);
333
+ switch (t) {
334
+ case "create": {
335
+ const code = this.srv.newCode();
336
+ const p = { peerId: 1, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-1` };
337
+ const l = {
338
+ code, gameId, netVersion, name: String(m.name), hostId: 1, epoch: 0, mode: String(m.mode), tickRate: Number(m.tickRate),
339
+ lateJoin: !!m.lateJoin, phase: "lobby", meta: m.meta ?? {}, visibility: String(m.visibility),
340
+ maxPlayers: Number(m.maxPlayers), peers: new Map([[1, p]]), next: 2, kicked: new Set(), reserved: new Map(),
341
+ };
342
+ this.srv.lobbies.set(code, l);
343
+ this.lobby = l;
344
+ this.peer = p;
345
+ return this.push(this.lobbyMsg(l, p));
346
+ }
347
+ case "join": {
348
+ const code = normalizeCode(String(m.code));
349
+ if (!code)
350
+ return this.err("bad_request", "bad code");
351
+ const l = this.srv.lobbies.get(code);
352
+ if (!l || l.gameId !== gameId)
353
+ return this.err("unknown_code");
354
+ if (l.netVersion !== netVersion)
355
+ return this.err("version_mismatch", "", { hostNetVersion: l.netVersion, yourNetVersion: netVersion });
356
+ if (l.phase === "in_game" && !l.lateJoin)
357
+ return this.err("in_game");
358
+ if (l.peers.size >= l.maxPlayers)
359
+ return this.err("lobby_full");
360
+ const id = l.next++;
361
+ const p = { peerId: id, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-${id}` };
362
+ l.peers.set(id, p);
363
+ this.lobby = l;
364
+ this.peer = p;
365
+ this.push(this.lobbyMsg(l, p));
366
+ return this.broadcast(l, { t: "peer_joined", peer: { peerId: id, name: p.name, joinedAt: p.joinedAt } }, id);
367
+ }
368
+ case "list": {
369
+ const items = [...this.srv.lobbies.values()]
370
+ .filter((l) => l.gameId === gameId && l.netVersion === netVersion && l.visibility === "public")
371
+ .map((l) => ({
372
+ code: l.code, gameId: l.gameId, netVersion: l.netVersion, name: l.name, hostName: l.peers.get(l.hostId)?.name ?? "",
373
+ players: l.peers.size, maxPlayers: l.maxPlayers, phase: l.phase, meta: l.meta, region: "mem", createdAt: 0,
374
+ }));
375
+ return this.push({ t: "lobbies", items });
376
+ }
377
+ case "leave":
378
+ return this.leaveLobby("left");
379
+ case "signal": {
380
+ const l = this.lobby;
381
+ if (!l || !this.peer)
382
+ return this.err("bad_request");
383
+ const to = l.peers.get(Number(m.to));
384
+ if (!to)
385
+ return this.err("bad_request", "no such peer");
386
+ return to.sock?.push({ t: "signal", from: this.peer.peerId, ...(m.sdp ? { sdp: m.sdp } : {}), ...(m.ice ? { ice: m.ice } : {}) });
387
+ }
388
+ case "meta":
389
+ case "phase": {
390
+ const l = this.lobby;
391
+ if (!l || !this.peer)
392
+ return;
393
+ if (l.hostId !== this.peer.peerId)
394
+ return this.err("not_host");
395
+ if (t === "meta")
396
+ l.meta = m.meta;
397
+ else
398
+ l.phase = String(m.phase);
399
+ for (const q of l.peers.values())
400
+ q.sock?.push(this.lobbyMsg(l, q));
401
+ return;
402
+ }
403
+ case "kick": {
404
+ const l = this.lobby;
405
+ if (!l || !this.peer)
406
+ return;
407
+ if (l.hostId !== this.peer.peerId)
408
+ return this.err("not_host");
409
+ const victim = l.peers.get(Number(m.peerId));
410
+ if (!victim || victim === this.peer)
411
+ return this.err("bad_request");
412
+ l.kicked.add(victim.peerId);
413
+ l.peers.delete(victim.peerId);
414
+ victim.sock?.push({ t: "error", code: "kicked", msg: "kicked" });
415
+ this.broadcast(l, { t: "peer_left", peerId: victim.peerId, reason: "kicked" });
416
+ return;
417
+ }
418
+ case "host_changed": {
419
+ const l = this.lobby;
420
+ if (!l || !this.peer)
421
+ return;
422
+ if (Number(m.hostId) === this.peer.peerId && Number(m.epoch) > l.epoch) {
423
+ l.hostId = this.peer.peerId;
424
+ l.epoch = Number(m.epoch);
425
+ this.broadcast(l, { t: "host_changed", hostId: l.hostId, epoch: l.epoch });
426
+ }
427
+ return;
428
+ }
429
+ default:
430
+ return;
431
+ }
432
+ }
433
+ }
434
+ /** Convenience: kill a socket as if the network dropped (server sees a timeout, others get peer_left). */
435
+ export function dropSocket(s) {
436
+ s.close();
437
+ }
438
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Test doubles: a manual clock, an in-memory lobby server speaking the lk1 protocol (spec/signaling.md, the parts the\n * client uses) and an in-memory peer network. Exported as `onelobby/testing`.\n */\nimport type { Clock } from \"./clock.js\";\nimport { CROCKFORD_ALPHABET, normalizeCode } from \"./crockford.js\";\nimport type {\n ChannelName,\n LinkFactory,\n LinkOptions,\n PeerLink,\n SignalPayload,\n SocketFactory,\n SocketLike,\n} from \"./transport.js\";\n\n// ---- clock ------------------------------------------------------------------------------------------------------\n\ninterface Timer {\n at: number;\n seq: number;\n fn: () => void;\n every?: number;\n dead?: boolean;\n}\n\n/** Deterministic clock: time only moves through `advance()`. Timers fire in (time, creation) order. */\nexport class ManualClock implements Clock {\n private t = 0;\n private seq = 0;\n private timers: Timer[] = [];\n\n now(): number {\n return this.t;\n }\n\n setInterval(fn: () => void, ms: number): () => void {\n const timer: Timer = { at: this.t + ms, seq: this.seq++, fn, every: Math.max(1, ms) };\n this.timers.push(timer);\n return () => {\n timer.dead = true;\n };\n }\n\n setTimeout(fn: () => void, ms: number): () => void {\n const timer: Timer = { at: this.t + ms, seq: this.seq++, fn };\n this.timers.push(timer);\n return () => {\n timer.dead = true;\n };\n }\n\n /** Advance time, firing due timers one at a time (microtasks flush between them when awaited via `run`). */\n advance(ms: number): void {\n const end = this.t + ms;\n for (;;) {\n this.timers = this.timers.filter((x) => !x.dead);\n let next: Timer | undefined;\n for (const x of this.timers) if (x.at <= end && (!next || x.at < next.at || (x.at === next.at && x.seq < next.seq))) next = x;\n if (!next) break;\n this.t = Math.max(this.t, next.at);\n if (next.every) {\n next.at += next.every;\n next.seq = this.seq++;\n } else {\n next.dead = true;\n }\n next.fn();\n }\n this.t = end;\n }\n\n /** Advance in `step` ms slices, letting promises and queued microtasks settle between slices. */\n async run(ms: number, step = 10): Promise<void> {\n let left = ms;\n await settle();\n while (left > 0) {\n const d = Math.min(step, left);\n this.advance(d);\n left -= d;\n await settle();\n }\n }\n}\n\n/** Let all pending microtasks (and chained promise continuations) run. */\nexport async function settle(rounds = 8): Promise<void> {\n for (let i = 0; i < rounds; i++) await Promise.resolve();\n // A macrotask boundary guarantees the whole microtask queue (delivery chains) has drained.\n await new Promise<void>((r) => ((globalThis as { setImmediate?: (f: () => void) => void }).setImmediate?.(r) ?? setTimeout(r, 0)));\n}\n\n// ---- in-memory peer network ---------------------------------------------------------------------------------------\n\nlet linkSerial = 0;\n\n/**\n * Pairs `PeerLink`s through the signaling path like WebRTC would: the offerer's SDP string names it, the answerer\n * resolves that name, and both open one microtask later. Messages are delivered FIFO, one microtask later.\n */\nexport class MemoryNetwork {\n private offers = new Map<string, MemoryLink>();\n /** Every link ever created (for assertions and fault injection). */\n readonly links: MemoryLink[] = [];\n /** Set to drop all traffic in one direction pair for fault injection: key `${from}->${to}` is not tracked; use link.mute(). */\n readonly factory: LinkFactory = { create: (opts) => new MemoryLink(this, opts) };\n\n /** @internal */\n register(offer: string, link: MemoryLink): void {\n this.offers.set(offer, link);\n this.links.push(link);\n }\n /** @internal */\n lookup(offer: string): MemoryLink | undefined {\n return this.offers.get(offer);\n }\n\n /** Kill every link whose far end is this identity-tagged factory owner (set `tag` via MemoryLink.owner). */\n killOwner(owner: string): void {\n for (const l of this.links) if (l.owner === owner) l.close();\n }\n\n /** Per-owner factory so tests can tell whose link is whose and cut one peer off. */\n factoryFor(owner: string): LinkFactory {\n return {\n create: (opts) => {\n const l = new MemoryLink(this, opts);\n l.owner = owner;\n return l;\n },\n };\n }\n}\n\nexport class MemoryLink implements PeerLink {\n readonly remoteId: number;\n isOpen = false;\n owner = \"\";\n onSignal: PeerLink[\"onSignal\"] = null;\n onOpen: PeerLink[\"onOpen\"] = null;\n onClose: PeerLink[\"onClose\"] = null;\n onMessage: PeerLink[\"onMessage\"] = null;\n onPath: PeerLink[\"onPath\"] = null;\n /** Bytes sent per channel, for assertions. */\n readonly sent: Record<ChannelName, number> = { reliable: 0, fast: 0, voice: 0 };\n /** While true, outgoing messages vanish (a frozen or partitioned peer). */\n blackhole = false;\n private peer: MemoryLink | null = null;\n private closed = false;\n private id = `mem-${++linkSerial}`;\n\n constructor(private net: MemoryNetwork, opts: LinkOptions) {\n this.remoteId = opts.remoteId;\n }\n\n startOffer(): void {\n this.net.register(this.id, this);\n queueMicrotask(() => this.onSignal?.({ sdp: { type: \"offer\", sdp: this.id } }));\n }\n\n handleSignal(s: SignalPayload): void {\n if (s.sdp?.type === \"offer\") {\n const offerer = this.net.lookup(s.sdp.sdp);\n this.net.links.push(this);\n if (!offerer) return;\n this.peer = offerer;\n queueMicrotask(() => this.onSignal?.({ sdp: { type: \"answer\", sdp: this.id } }));\n queueMicrotask(() => offerer.accept(this));\n queueMicrotask(() => this.open());\n }\n }\n\n /** @internal */\n accept(answerer: MemoryLink): void {\n this.peer = answerer;\n this.open();\n }\n\n private open(): void {\n if (this.closed || this.isOpen) return;\n this.isOpen = true;\n this.onOpen?.();\n this.onPath?.(\"direct\");\n }\n\n send(channel: ChannelName, bytes: Uint8Array): void {\n if (!this.isOpen || this.closed) return;\n this.sent[channel] += bytes.length;\n if (this.blackhole) return;\n const peer = this.peer;\n const copy = bytes.slice();\n queueMicrotask(() => {\n if (peer && peer.isOpen && !peer.closed && !this.closed) peer.onMessage?.(channel, copy);\n });\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.isOpen = false;\n const peer = this.peer;\n queueMicrotask(() => {\n this.onClose?.(\"closed\");\n if (peer && !peer.closed) peer.closeFromRemote();\n });\n }\n\n private closeFromRemote(): void {\n if (this.closed) return;\n this.closed = true;\n this.isOpen = false;\n this.onClose?.(\"remote closed\");\n }\n}\n\n// ---- in-memory lobby server ---------------------------------------------------------------------------------------\n\ntype Json = Record<string, unknown>;\n\ninterface MPeer {\n peerId: number;\n name: string;\n joinedAt: number;\n sock: MemorySocket | null;\n token: string;\n}\ninterface MLobby {\n code: string;\n gameId: string;\n netVersion: string;\n name: string;\n hostId: number;\n epoch: number;\n mode: string;\n tickRate: number;\n lateJoin: boolean;\n phase: string;\n meta: Record<string, string>;\n visibility: string;\n maxPlayers: number;\n peers: Map<number, MPeer>;\n next: number;\n kicked: Set<number>;\n reserved: Map<string, MPeer>;\n}\n\n/** Minimal onelobby-server (lk1) for client tests. Not a conformance target: stream A owns the real server. */\nexport class MemoryServer {\n readonly lobbies = new Map<string, MLobby>();\n /** Every socket ever opened (to drop one from a test). */\n readonly sockets: MemorySocket[] = [];\n readonly socketFactory: SocketFactory = () => this.connect();\n private n = 0;\n iceServers = [{ urls: [\"stun:stun.example:3478\"] }];\n /** Log of every client message, for assertions. */\n readonly received: Json[] = [];\n\n connect(): MemorySocket {\n const s = new MemorySocket(this);\n this.sockets.push(s);\n return s;\n }\n\n /** @internal */\n newCode(): string {\n let x = 0x9e3779b1 * (++this.n + 17);\n let s = \"\";\n for (let i = 0; i < 6; i++) {\n x = (Math.imul(x, 1103515245) + 12345) >>> 0;\n s += CROCKFORD_ALPHABET[(x >>> 16) & 31];\n }\n return this.lobbies.has(s) ? this.newCode() : s;\n }\n}\n\nexport class MemorySocket implements SocketLike {\n readyState = 0;\n onopen: SocketLike[\"onopen\"] = null;\n onmessage: SocketLike[\"onmessage\"] = null;\n onclose: SocketLike[\"onclose\"] = null;\n onerror: SocketLike[\"onerror\"] = null;\n\n private hello: Json | null = null;\n private lobby: MLobby | null = null;\n private peer: MPeer | null = null;\n private closed = false;\n\n constructor(private srv: MemoryServer) {\n queueMicrotask(() => {\n this.readyState = 1;\n this.onopen?.();\n });\n }\n\n // ---- socket API\n send(data: string): void {\n if (this.closed) return;\n const m = JSON.parse(data) as Json;\n queueMicrotask(() => this.handle(m));\n }\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.readyState = 3;\n this.leaveLobby(\"timeout\");\n queueMicrotask(() => this.onclose?.());\n }\n\n /** @internal server -> this client */\n push(m: Json): void {\n if (this.closed) return;\n queueMicrotask(() => this.onmessage?.({ data: JSON.stringify(m) }));\n }\n\n private err(code: string, msg = \"\", data?: Json) {\n this.push({ t: \"error\", code, msg, ...(data ? { data } : {}) });\n }\n\n private lobbyMsg(l: MLobby, p: MPeer): Json {\n return {\n t: \"lobby\",\n code: l.code,\n gameId: l.gameId,\n netVersion: l.netVersion,\n hostId: l.hostId,\n you: p.peerId,\n mode: l.mode,\n tickRate: l.tickRate,\n lateJoin: l.lateJoin,\n phase: l.phase,\n meta: l.meta,\n peers: [...l.peers.values()].map((q) => ({ peerId: q.peerId, name: q.name, joinedAt: q.joinedAt })),\n resumeToken: p.token,\n name: l.name,\n maxPlayers: l.maxPlayers,\n visibility: l.visibility,\n };\n }\n\n private broadcast(l: MLobby, m: Json, except?: number) {\n for (const q of l.peers.values()) if (q.peerId !== except) q.sock?.push(m);\n }\n\n private leaveLobby(reason: string) {\n const l = this.lobby;\n const p = this.peer;\n if (!l || !p) return;\n this.lobby = null;\n this.peer = null;\n p.sock = null;\n l.peers.delete(p.peerId);\n if (reason === \"timeout\") l.reserved.set(p.token, p); // slot kept for resume\n if (l.peers.size === 0) {\n this.srv.lobbies.delete(l.code);\n return;\n }\n this.broadcast(l, { t: \"peer_left\", peerId: p.peerId, reason });\n if (l.hostId === p.peerId) {\n l.hostId = Math.min(...l.peers.keys());\n l.epoch++;\n this.broadcast(l, { t: \"host_changed\", hostId: l.hostId, epoch: l.epoch });\n }\n }\n\n private handle(m: Json) {\n if (this.closed) return;\n this.srv.received.push(m);\n const t = String(m.t);\n if (t === \"ping\") return this.push({ t: \"pong\", n: m.n });\n if (t === \"hello\") {\n if (this.hello) return this.err(\"bad_request\", \"second hello\");\n this.hello = m;\n const tok = typeof m.resumeToken === \"string\" ? m.resumeToken : \"\";\n for (const l of this.srv.lobbies.values()) {\n const rp = l.reserved.get(tok);\n if (!rp) continue;\n l.reserved.delete(tok);\n rp.sock = this;\n rp.token = tok + \"+\";\n l.peers.set(rp.peerId, rp);\n this.lobby = l;\n this.peer = rp;\n this.push({ t: \"welcome\", peerId: rp.peerId, ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });\n this.push(this.lobbyMsg(l, rp));\n return this.broadcast(l, { t: \"peer_joined\", peer: { peerId: rp.peerId, name: rp.name, joinedAt: rp.joinedAt } }, rp.peerId);\n }\n return this.push({ t: \"welcome\", ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });\n }\n if (!this.hello) return this.err(\"bad_request\", \"hello first\");\n const gameId = String(this.hello.gameId);\n const netVersion = String(this.hello.netVersion);\n switch (t) {\n case \"create\": {\n const code = this.srv.newCode();\n const p: MPeer = { peerId: 1, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-1` };\n const l: MLobby = {\n code, gameId, netVersion, name: String(m.name), hostId: 1, epoch: 0, mode: String(m.mode), tickRate: Number(m.tickRate),\n lateJoin: !!m.lateJoin, phase: \"lobby\", meta: (m.meta as Record<string, string>) ?? {}, visibility: String(m.visibility),\n maxPlayers: Number(m.maxPlayers), peers: new Map([[1, p]]), next: 2, kicked: new Set(), reserved: new Map(),\n };\n this.srv.lobbies.set(code, l);\n this.lobby = l;\n this.peer = p;\n return this.push(this.lobbyMsg(l, p));\n }\n case \"join\": {\n const code = normalizeCode(String(m.code));\n if (!code) return this.err(\"bad_request\", \"bad code\");\n const l = this.srv.lobbies.get(code);\n if (!l || l.gameId !== gameId) return this.err(\"unknown_code\");\n if (l.netVersion !== netVersion) return this.err(\"version_mismatch\", \"\", { hostNetVersion: l.netVersion, yourNetVersion: netVersion });\n if (l.phase === \"in_game\" && !l.lateJoin) return this.err(\"in_game\");\n if (l.peers.size >= l.maxPlayers) return this.err(\"lobby_full\");\n const id = l.next++;\n const p: MPeer = { peerId: id, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-${id}` };\n l.peers.set(id, p);\n this.lobby = l;\n this.peer = p;\n this.push(this.lobbyMsg(l, p));\n return this.broadcast(l, { t: \"peer_joined\", peer: { peerId: id, name: p.name, joinedAt: p.joinedAt } }, id);\n }\n case \"list\": {\n const items = [...this.srv.lobbies.values()]\n .filter((l) => l.gameId === gameId && l.netVersion === netVersion && l.visibility === \"public\")\n .map((l) => ({\n code: l.code, gameId: l.gameId, netVersion: l.netVersion, name: l.name, hostName: l.peers.get(l.hostId)?.name ?? \"\",\n players: l.peers.size, maxPlayers: l.maxPlayers, phase: l.phase, meta: l.meta, region: \"mem\", createdAt: 0,\n }));\n return this.push({ t: \"lobbies\", items });\n }\n case \"leave\":\n return this.leaveLobby(\"left\");\n case \"signal\": {\n const l = this.lobby;\n if (!l || !this.peer) return this.err(\"bad_request\");\n const to = l.peers.get(Number(m.to));\n if (!to) return this.err(\"bad_request\", \"no such peer\");\n return to.sock?.push({ t: \"signal\", from: this.peer.peerId, ...(m.sdp ? { sdp: m.sdp } : {}), ...(m.ice ? { ice: m.ice } : {}) });\n }\n case \"meta\":\n case \"phase\": {\n const l = this.lobby;\n if (!l || !this.peer) return;\n if (l.hostId !== this.peer.peerId) return this.err(\"not_host\");\n if (t === \"meta\") l.meta = m.meta as Record<string, string>;\n else l.phase = String(m.phase);\n for (const q of l.peers.values()) q.sock?.push(this.lobbyMsg(l, q));\n return;\n }\n case \"kick\": {\n const l = this.lobby;\n if (!l || !this.peer) return;\n if (l.hostId !== this.peer.peerId) return this.err(\"not_host\");\n const victim = l.peers.get(Number(m.peerId));\n if (!victim || victim === this.peer) return this.err(\"bad_request\");\n l.kicked.add(victim.peerId);\n l.peers.delete(victim.peerId);\n victim.sock?.push({ t: \"error\", code: \"kicked\", msg: \"kicked\" });\n this.broadcast(l, { t: \"peer_left\", peerId: victim.peerId, reason: \"kicked\" });\n return;\n }\n case \"host_changed\": {\n const l = this.lobby;\n if (!l || !this.peer) return;\n if (Number(m.hostId) === this.peer.peerId && Number(m.epoch) > l.epoch) {\n l.hostId = this.peer.peerId;\n l.epoch = Number(m.epoch);\n this.broadcast(l, { t: \"host_changed\", hostId: l.hostId, epoch: l.epoch });\n }\n return;\n }\n default:\n return;\n }\n }\n}\n\n/** Convenience: kill a socket as if the network dropped (server sees a timeout, others get peer_left). */\nexport function dropSocket(s: SocketLike): void {\n s.close();\n}\n"]}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Transport seams. The protocol logic never touches RTCPeerConnection or WebSocket directly, so the whole
3
+ * lobby/lockstep/chat/voice stack runs under vitest against `MemoryNetwork` / `MemoryServer` (see testing.ts).
4
+ */
5
+ export type ChannelName = "reliable" | "fast" | "voice";
6
+ export declare const CHANNEL_IDS: Record<ChannelName, number>;
7
+ export interface IceServer {
8
+ urls: string | string[];
9
+ username?: string;
10
+ credential?: string;
11
+ ttl?: number;
12
+ }
13
+ export interface SdpInit {
14
+ type: "offer" | "answer";
15
+ sdp: string;
16
+ }
17
+ export interface IceInit {
18
+ candidate: string;
19
+ sdpMid?: string | null;
20
+ sdpMLineIndex?: number | null;
21
+ }
22
+ export interface SignalPayload {
23
+ sdp?: SdpInit;
24
+ ice?: IceInit;
25
+ }
26
+ /** One peer connection with the three negotiated channels (s.8). */
27
+ export interface PeerLink {
28
+ readonly remoteId: number;
29
+ readonly isOpen: boolean;
30
+ /** Fired for each local SDP/ICE blob that must reach the remote through signaling. */
31
+ onSignal: ((s: SignalPayload) => void) | null;
32
+ /** Fired once, when all three channels are open. */
33
+ onOpen: (() => void) | null;
34
+ /** Fired once when the link dies (after open or while connecting). */
35
+ onClose: ((reason: string) => void) | null;
36
+ onMessage: ((channel: ChannelName, bytes: Uint8Array) => void) | null;
37
+ /** Fired once after ICE completes: whether the selected candidate pair uses a TURN relay (reported to the server). */
38
+ onPath?: ((path: "direct" | "relayed") => void) | null;
39
+ /** The offerer calls this to start negotiation. The answerer just feeds handleSignal. */
40
+ startOffer(): void;
41
+ handleSignal(s: SignalPayload): void | Promise<void>;
42
+ send(channel: ChannelName, bytes: Uint8Array): void;
43
+ close(): void;
44
+ }
45
+ export interface LinkOptions {
46
+ remoteId: number;
47
+ iceServers: IceServer[];
48
+ relayOnly: boolean;
49
+ /**
50
+ * Direct-IP host (docs/direct-ip.md s.2): bind ICE to these local UDP ports. One port (begin = end) also turns on
51
+ * ICE UDP mux so every peer shares it. Honoured by libdatachannel-based RTCPeerConnection (node-datachannel);
52
+ * browsers ignore it.
53
+ */
54
+ portRange?: {
55
+ begin: number;
56
+ end: number;
57
+ };
58
+ /**
59
+ * Direct-IP joiner: remote ICE candidate lines (`candidate:1 1 UDP ... typ host`) added after the remote
60
+ * description, for the address the player typed or pasted as a code.
61
+ */
62
+ extraRemoteCandidates?: string[];
63
+ }
64
+ export interface LinkFactory {
65
+ create(opts: LinkOptions): PeerLink;
66
+ }
67
+ /** Subset of the WebSocket API the signaling client needs. */
68
+ export interface SocketLike {
69
+ readonly readyState: number;
70
+ onopen: ((ev?: unknown) => void) | null;
71
+ onmessage: ((ev: {
72
+ data: unknown;
73
+ }) => void) | null;
74
+ onclose: ((ev?: unknown) => void) | null;
75
+ onerror: ((ev?: unknown) => void) | null;
76
+ send(data: string): void;
77
+ close(code?: number, reason?: string): void;
78
+ }
79
+ export type SocketFactory = (url: string) => SocketLike;
80
+ export declare const defaultSocketFactory: SocketFactory;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Transport seams. The protocol logic never touches RTCPeerConnection or WebSocket directly, so the whole
3
+ * lobby/lockstep/chat/voice stack runs under vitest against `MemoryNetwork` / `MemoryServer` (see testing.ts).
4
+ */
5
+ export const CHANNEL_IDS = { reliable: 0, fast: 1, voice: 2 };
6
+ export const defaultSocketFactory = (url) => new WebSocket(url);
7
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,CAAC,MAAM,WAAW,GAAgC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AA6E3F,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,GAAG,CAA0B,CAAC","sourcesContent":["/**\n * Transport seams. The protocol logic never touches RTCPeerConnection or WebSocket directly, so the whole\n * lobby/lockstep/chat/voice stack runs under vitest against `MemoryNetwork` / `MemoryServer` (see testing.ts).\n */\n\nexport type ChannelName = \"reliable\" | \"fast\" | \"voice\";\n\nexport const CHANNEL_IDS: Record<ChannelName, number> = { reliable: 0, fast: 1, voice: 2 };\n\nexport interface IceServer {\n urls: string | string[];\n username?: string;\n credential?: string;\n ttl?: number;\n}\n\nexport interface SdpInit {\n type: \"offer\" | \"answer\";\n sdp: string;\n}\nexport interface IceInit {\n candidate: string;\n sdpMid?: string | null;\n sdpMLineIndex?: number | null;\n}\n\nexport interface SignalPayload {\n sdp?: SdpInit;\n ice?: IceInit;\n}\n\n/** One peer connection with the three negotiated channels (s.8). */\nexport interface PeerLink {\n readonly remoteId: number;\n readonly isOpen: boolean;\n /** Fired for each local SDP/ICE blob that must reach the remote through signaling. */\n onSignal: ((s: SignalPayload) => void) | null;\n /** Fired once, when all three channels are open. */\n onOpen: (() => void) | null;\n /** Fired once when the link dies (after open or while connecting). */\n onClose: ((reason: string) => void) | null;\n onMessage: ((channel: ChannelName, bytes: Uint8Array) => void) | null;\n /** Fired once after ICE completes: whether the selected candidate pair uses a TURN relay (reported to the server). */\n onPath?: ((path: \"direct\" | \"relayed\") => void) | null;\n /** The offerer calls this to start negotiation. The answerer just feeds handleSignal. */\n startOffer(): void;\n handleSignal(s: SignalPayload): void | Promise<void>;\n send(channel: ChannelName, bytes: Uint8Array): void;\n close(): void;\n}\n\nexport interface LinkOptions {\n remoteId: number;\n iceServers: IceServer[];\n relayOnly: boolean;\n /**\n * Direct-IP host (docs/direct-ip.md s.2): bind ICE to these local UDP ports. One port (begin = end) also turns on\n * ICE UDP mux so every peer shares it. Honoured by libdatachannel-based RTCPeerConnection (node-datachannel);\n * browsers ignore it.\n */\n portRange?: { begin: number; end: number };\n /**\n * Direct-IP joiner: remote ICE candidate lines (`candidate:1 1 UDP ... typ host`) added after the remote\n * description, for the address the player typed or pasted as a code.\n */\n extraRemoteCandidates?: string[];\n}\n\nexport interface LinkFactory {\n create(opts: LinkOptions): PeerLink;\n}\n\n/** Subset of the WebSocket API the signaling client needs. */\nexport interface SocketLike {\n readonly readyState: number;\n onopen: ((ev?: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n onclose: ((ev?: unknown) => void) | null;\n onerror: ((ev?: unknown) => void) | null;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n}\nexport type SocketFactory = (url: string) => SocketLike;\n\nexport const defaultSocketFactory: SocketFactory = (url) => new WebSocket(url) as unknown as SocketLike;\n"]}