@genex-ai/multiplayer 0.16.2 → 0.17.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.
package/dist/index.js CHANGED
@@ -1,1812 +1,10 @@
1
- // src/index.ts
2
- import { Client, getStateCallbacks } from "@colyseus/sdk";
3
-
4
- // src/ice-request.ts
5
- function createIceRequester(send, timeoutMs) {
6
- let inFlight = null;
7
- let settle = null;
8
- return {
9
- request() {
10
- if (inFlight) return inFlight;
11
- inFlight = new Promise((resolve) => {
12
- let timer;
13
- const done = (cfg) => {
14
- if (!settle) return;
15
- settle = null;
16
- inFlight = null;
17
- if (timer) clearTimeout(timer);
18
- resolve(cfg);
19
- };
20
- settle = done;
21
- timer = setTimeout(() => done(null), timeoutMs);
22
- send();
23
- });
24
- return inFlight;
25
- },
26
- deliver(cfg) {
27
- settle?.(cfg);
28
- },
29
- get pending() {
30
- return inFlight !== null;
31
- }
32
- };
33
- }
34
-
35
- // src/voice.ts
36
- var NOT_DELEGATED = "The microphone was refused by the page that frames this game, not by the player: the frame was not given it, so the browser never asked. On its Genex page a build is given the microphone only when it declares it. An up-to-date genex CLI declares it at `genex preview` and `genex publish` when the game's own source calls voice.join(); to declare it by hand, add \"microphone\" to genex.capabilities in the game's package.json. Then preview or publish again.";
37
- var VoiceNotDelegatedError = class extends Error {
38
- constructor(cause) {
39
- super(NOT_DELEGATED, { cause });
40
- this.name = "VoiceNotDelegatedError";
41
- }
42
- };
43
- var DEFAULT_ICE = [{ urls: "stun:stun.l.google.com:19302" }];
44
- var SPEAKING_POLL_MS = 120;
45
- var DEFAULT_SPEAKING_THRESHOLD = 0.02;
46
- var SPEAKING_RELEASE_RATIO = 0.6;
47
- var VoiceMesh = class {
48
- transport;
49
- peers = /* @__PURE__ */ new Map();
50
- listeners = /* @__PURE__ */ new Set();
51
- roster = [];
52
- local = null;
53
- audio = null;
54
- localAnalyser = null;
55
- levelTimer = null;
56
- iceServers = DEFAULT_ICE;
57
- spatial = false;
58
- threshold = DEFAULT_SPEAKING_THRESHOLD;
59
- micOn = true;
60
- selfSpeaking = false;
61
- running = false;
62
- // Peering is HELD between acquiring the mic and announcing the join, because both things
63
- // that happen in that window must land first: the relay's TURN credentials (a peer built
64
- // without them is stuck on STUN for its whole life), and our own `__voice:join` (the relay
65
- // drops a signal from a session that is not yet in the roster, and a dropped offer is never
66
- // retried). Without the hold, another player joining voice in that window patches `__voice`
67
- // and builds us a peer that can neither relay nor negotiate.
68
- peeringHeld = false;
69
- constructor(transport) {
70
- this.transport = transport;
71
- }
72
- get active() {
73
- return this.running;
74
- }
75
- get micEnabled() {
76
- return this.micOn;
77
- }
78
- get speaking() {
79
- return this.selfSpeaking;
80
- }
81
- get participants() {
82
- return [...this.peers.values()].map((p) => ({
83
- id: p.id,
84
- name: this.transport.nameOf(p.id),
85
- stream: p.stream,
86
- speaking: p.speaking,
87
- muted: p.muted,
88
- volume: p.volume,
89
- connection: p.connection
90
- }));
91
- }
92
- onChange(cb) {
93
- this.listeners.add(cb);
94
- return () => this.listeners.delete(cb);
95
- }
96
- /** Acquire the microphone. Throws if the user denies it or no device exists — the caller
97
- * surfaces that; it is a normal outcome, not a bug. The one refusal that IS a bug throws a
98
- * VoiceNotDelegatedError instead: the page framing the game withheld the microphone, because
99
- * the build never declared it. Must be called from a user gesture: browsers gate both the
100
- * permission prompt and audio playback on one. */
101
- async start(options = {}, holdPeering = false) {
102
- if (this.running) return;
103
- this.peeringHeld = holdPeering;
104
- this.iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE;
105
- this.spatial = options.spatial === true;
106
- this.threshold = options.speakingThreshold ?? DEFAULT_SPEAKING_THRESHOLD;
107
- this.micOn = options.startMuted !== true;
108
- try {
109
- this.local = await navigator.mediaDevices.getUserMedia({
110
- audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
111
- video: false
112
- });
113
- } catch (err) {
114
- throw micRefusal(err);
115
- }
116
- for (const t of this.local.getAudioTracks()) t.enabled = this.micOn;
117
- this.audio = new AudioContext();
118
- this.localAnalyser = this.makeAnalyser(this.local);
119
- this.running = true;
120
- this.levelTimer = setInterval(() => this.pollLevels(), SPEAKING_POLL_MS);
121
- }
122
- /** Tear everything down. Safe to call when not running. */
123
- stop() {
124
- this.running = false;
125
- this.peeringHeld = false;
126
- if (this.levelTimer) {
127
- clearInterval(this.levelTimer);
128
- this.levelTimer = null;
129
- }
130
- for (const id of [...this.peers.keys()]) this.destroyPeer(id);
131
- for (const t of this.local?.getTracks() ?? []) t.stop();
132
- this.local = null;
133
- this.localAnalyser = null;
134
- void this.audio?.close().catch(() => {
135
- });
136
- this.audio = null;
137
- this.roster = [];
138
- this.selfSpeaking = false;
139
- this.emit();
140
- }
141
- /**
142
- * Swap in ICE servers obtained after `start()` — in practice the relay's short-lived TURN
143
- * credentials, which are fetched between acquiring the mic and announcing the join so the
144
- * network round trip never spends the user gesture `getUserMedia` needs.
145
- *
146
- * Peers built from here on use them. Live ones are updated too, but `setConfiguration`
147
- * governs FUTURE gathering only: an allocation already relaying media keeps the credential
148
- * it was created with. That is why the credential's TTL is sized to outlast a session
149
- * rather than to reach the next refresh.
150
- */
151
- setIceServers(servers) {
152
- if (!servers.length) return;
153
- this.iceServers = servers;
154
- for (const peer of this.peers.values()) {
155
- try {
156
- peer.pc.setConfiguration({ iceServers: servers });
157
- } catch {
158
- }
159
- }
160
- }
161
- /**
162
- * Release the hold taken by `start(…, true)` and reconcile whatever roster arrived while it
163
- * was up. The caller MUST reach this on every path, including the one where no credentials
164
- * came back: a mesh that never peers is worse than one peering without TURN.
165
- */
166
- releasePeering() {
167
- if (!this.peeringHeld) return;
168
- this.peeringHeld = false;
169
- this.setRoster(this.roster);
170
- }
171
- /** The relay's authoritative roster changed: open connections to newcomers, drop leavers. */
172
- setRoster(ids) {
173
- this.roster = ids;
174
- if (!this.running) return;
175
- if (this.peeringHeld) return;
176
- const self = this.transport.selfId();
177
- const wanted = new Set(ids.filter((id) => id !== self));
178
- for (const id of wanted) if (!this.peers.has(id)) this.createPeer(id);
179
- for (const id of [...this.peers.keys()]) if (!wanted.has(id)) this.destroyPeer(id);
180
- this.emit();
181
- }
182
- /** Push-to-talk. Gates the track rather than renegotiating — instant, and it keeps the
183
- * peer connections warm so the first syllable after a key press is not clipped. */
184
- setMicEnabled(on) {
185
- this.micOn = on;
186
- for (const t of this.local?.getAudioTracks() ?? []) t.enabled = on;
187
- if (!on && this.selfSpeaking) {
188
- this.selfSpeaking = false;
189
- this.emit();
190
- }
191
- }
192
- mute(id) {
193
- this.setMuted(id, true);
194
- }
195
- unmute(id) {
196
- this.setMuted(id, false);
197
- }
198
- setVolume(id, volume) {
199
- const p = this.peers.get(id);
200
- if (!p) return;
201
- p.volume = Math.max(0, Math.min(1, volume));
202
- if (p.el) p.el.volume = p.volume;
203
- this.emit();
204
- }
205
- /** A signal arrived from `from` (the relay stamped it — it cannot be spoofed). */
206
- async handleSignal(from, raw) {
207
- if (!this.running) return;
208
- const signal = raw;
209
- if (!signal || typeof signal !== "object" || typeof signal.t !== "string") return;
210
- const peer = this.peers.get(from) ?? (this.roster.includes(from) ? this.createPeer(from) : void 0);
211
- if (!peer) return;
212
- try {
213
- if (signal.t === "offer") {
214
- await peer.pc.setRemoteDescription(signal.sdp);
215
- await this.flushIce(peer);
216
- const answer = await peer.pc.createAnswer();
217
- await peer.pc.setLocalDescription(answer);
218
- this.transport.sendSignal(from, { t: "answer", sdp: answer });
219
- } else if (signal.t === "answer") {
220
- if (peer.pc.signalingState !== "have-local-offer") return;
221
- await peer.pc.setRemoteDescription(signal.sdp);
222
- await this.flushIce(peer);
223
- } else if (signal.t === "ice") {
224
- if (peer.pc.remoteDescription) await peer.pc.addIceCandidate(signal.c);
225
- else peer.pendingIce.push(signal.c);
226
- }
227
- } catch (err) {
228
- console.warn(`[multiplayer] voice signaling failed with ${from}`, err);
229
- }
230
- }
231
- // ---- internals ----
232
- setMuted(id, muted) {
233
- const p = this.peers.get(id);
234
- if (!p || p.muted === muted) return;
235
- p.muted = muted;
236
- if (p.el) p.el.muted = muted || this.spatial;
237
- if (muted && p.speaking) p.speaking = false;
238
- this.emit();
239
- }
240
- createPeer(id) {
241
- const pc = new RTCPeerConnection({ iceServers: this.iceServers });
242
- const peer = {
243
- id,
244
- pc,
245
- el: null,
246
- stream: null,
247
- analyser: null,
248
- pendingIce: [],
249
- speaking: false,
250
- muted: false,
251
- volume: 1,
252
- connection: "connecting"
253
- };
254
- this.peers.set(id, peer);
255
- for (const track of this.local?.getAudioTracks() ?? []) {
256
- if (this.local) pc.addTrack(track, this.local);
257
- }
258
- pc.onicecandidate = (e) => {
259
- if (e.candidate) this.transport.sendSignal(id, { t: "ice", c: e.candidate.toJSON() });
260
- };
261
- pc.ontrack = (e) => {
262
- const stream = e.streams[0] ?? new MediaStream([e.track]);
263
- peer.stream = stream;
264
- peer.el = this.attachSink(stream, peer);
265
- peer.analyser = this.makeAnalyser(stream);
266
- this.emit();
267
- };
268
- pc.onconnectionstatechange = () => {
269
- const s = pc.connectionState;
270
- const next = s === "connected" ? "connected" : s === "failed" || s === "closed" ? "failed" : "connecting";
271
- if (next !== peer.connection) {
272
- peer.connection = next;
273
- if (next === "failed") {
274
- const relayed = this.iceServers.some((s2) => hasTurnUrl(s2));
275
- console.warn(
276
- `[multiplayer] voice connection to ${id} failed` + (relayed ? "" : " \u2014 no TURN in the ICE list (relay offered none?)")
277
- );
278
- }
279
- this.emit();
280
- }
281
- };
282
- if (this.transport.selfId() < id) void this.offer(peer);
283
- return peer;
284
- }
285
- async offer(peer) {
286
- try {
287
- const offer = await peer.pc.createOffer();
288
- await peer.pc.setLocalDescription(offer);
289
- this.transport.sendSignal(peer.id, { t: "offer", sdp: offer });
290
- } catch (err) {
291
- console.warn(`[multiplayer] voice offer to ${peer.id} failed`, err);
292
- }
293
- }
294
- async flushIce(peer) {
295
- const queued = peer.pendingIce.splice(0);
296
- for (const c of queued) {
297
- try {
298
- await peer.pc.addIceCandidate(c);
299
- } catch {
300
- }
301
- }
302
- }
303
- destroyPeer(id) {
304
- const peer = this.peers.get(id);
305
- if (!peer) return;
306
- this.peers.delete(id);
307
- peer.pc.onicecandidate = null;
308
- peer.pc.ontrack = null;
309
- peer.pc.onconnectionstatechange = null;
310
- try {
311
- peer.pc.close();
312
- } catch {
313
- }
314
- if (peer.el) {
315
- peer.el.srcObject = null;
316
- peer.el.remove();
317
- }
318
- }
319
- /** Attach the stream to an element. Required even in spatial mode (see VoiceJoinOptions). */
320
- attachSink(stream, peer) {
321
- const el = document.createElement("audio");
322
- el.autoplay = true;
323
- el.srcObject = stream;
324
- el.muted = this.spatial || peer.muted;
325
- el.volume = peer.volume;
326
- el.style.display = "none";
327
- document.body.appendChild(el);
328
- void el.play().catch((err) => {
329
- console.warn("[multiplayer] voice playback was blocked \u2014 join voice from a click/tap", err);
330
- });
331
- return el;
332
- }
333
- makeAnalyser(stream) {
334
- if (!this.audio) return null;
335
- try {
336
- const src = this.audio.createMediaStreamSource(stream);
337
- const analyser = this.audio.createAnalyser();
338
- analyser.fftSize = 512;
339
- src.connect(analyser);
340
- return analyser;
341
- } catch {
342
- return null;
343
- }
344
- }
345
- pollLevels() {
346
- let changed = false;
347
- const localLevel = this.micOn ? rms(this.localAnalyser) : 0;
348
- const nextSelf = this.crossed(localLevel, this.selfSpeaking);
349
- if (nextSelf !== this.selfSpeaking) {
350
- this.selfSpeaking = nextSelf;
351
- changed = true;
352
- }
353
- for (const peer of this.peers.values()) {
354
- const next = peer.muted ? false : this.crossed(rms(peer.analyser), peer.speaking);
355
- if (next !== peer.speaking) {
356
- peer.speaking = next;
357
- changed = true;
358
- }
359
- }
360
- if (changed) this.emit();
361
- }
362
- /** Threshold with hysteresis so a name tag does not strobe between syllables. */
363
- crossed(level, was) {
364
- return level >= (was ? this.threshold * SPEAKING_RELEASE_RATIO : this.threshold);
365
- }
366
- emit() {
367
- for (const cb of [...this.listeners]) {
368
- try {
369
- cb();
370
- } catch (err) {
371
- console.error("[multiplayer] a voice listener threw", err);
372
- }
373
- }
374
- }
375
- };
376
- function isFramed() {
377
- try {
378
- return window.self !== window.top;
379
- } catch {
380
- return true;
381
- }
382
- }
383
- function micRefusal(err) {
384
- if (err?.name !== "NotAllowedError") return err;
385
- try {
386
- if (!isFramed()) return err;
387
- const policy = document.featurePolicy;
388
- if (policy?.allowsFeature?.("microphone") !== false) return err;
389
- } catch {
390
- return err;
391
- }
392
- return new VoiceNotDelegatedError(err);
393
- }
394
- function hasTurnUrl(server) {
395
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
396
- return urls.some((u) => typeof u === "string" && u.startsWith("turn"));
397
- }
398
- function rms(analyser) {
399
- if (!analyser) return 0;
400
- const buf = new Uint8Array(analyser.fftSize);
401
- analyser.getByteTimeDomainData(buf);
402
- let sum = 0;
403
- for (let i = 0; i < buf.length; i++) {
404
- const v = ((buf[i] ?? 128) - 128) / 128;
405
- sum += v * v;
406
- }
407
- return Math.sqrt(sum / buf.length);
408
- }
409
-
410
- // src/index.ts
411
- var RELAY_ROOM_TYPE = "play";
412
- var QUEUE_ROOM_TYPE = "queue";
413
- var MATCH_ROOM_TYPE = "match";
414
- var INTERP_RATIO = 1.5;
415
- var INTERP_MIN_MS = 90;
416
- var INTERP_MAX_MS = 300;
417
- var JITTER_SAFETY = 1.25;
418
- var POISON_GUARD_MS = 2e3;
419
- var INTERP_EASE = 0.1;
420
- var MAX_EXTRAP_MS = 250;
421
- var ROT_EXTRAP_MS = 150;
422
- var CLOCK_WIN = 80;
423
- var SMOOTH_TAU_POS = 30;
424
- var SMOOTH_TAU_ROT = 70;
425
- var SEND_TIME_KEY = "__t";
426
- var RESERVED_PREFIX = "__";
427
- var OBJ_PREFIX = "__obj:";
428
- var HOST_KEY = "__host";
429
- var MATCH_KEY = "__match";
430
- var INPUT_MSG = "__input";
431
- var CAPABILITIES_MSG = "__capabilities";
432
- var CAPABILITIES_REQUEST_MSG = "__capabilities:request";
433
- var CHAT_MSG = "__chat";
434
- var CHAT_HISTORY_MSG = "__chat:history";
435
- var CHAT_HISTORY_REQUEST_MSG = "__chat:history:request";
436
- var VOICE_KEY = "__voice";
437
- var VOICE_JOIN_MSG = "__voice:join";
438
- var VOICE_LEAVE_MSG = "__voice:leave";
439
- var VOICE_SIGNAL_MSG = "__voice:signal";
440
- var VOICE_REJECTED_MSG = "__voice:rejected";
441
- var VOICE_ICE_REQUEST_MSG = "__voice:ice:request";
442
- var VOICE_ICE_MSG = "__voice:ice";
443
- var ICE_REQUEST_TIMEOUT_MS = 3e3;
444
- var OBJECT_CONTROL_RESULT_MSG = "__object:result";
445
- var OBJECT_CONTROL_VERSION = 2;
446
- var STATE_SNAP_MSG = "__state:snap";
447
- var ROOM_OPEN_MSG = "__room:open";
448
- var OBJECT_SNAP_MSG = "__object:snap";
449
- var MP_PROTOCOL = 2;
450
- async function relayClient(opts) {
451
- if (opts.url) return new Client(opts.url);
452
- if (opts.urls.length > 1) return Client.selectByLatency(opts.urls);
453
- const url = opts.urls[0];
454
- if (!url) throw new Error("genex multiplayer: `urls` must contain at least one relay URL");
455
- return new Client(url);
456
- }
457
- var resolveAuth = (auth) => typeof auth === "function" ? auth() : auth;
458
- function parseData(s) {
459
- if (!s) return { state: {} };
460
- try {
461
- const v = JSON.parse(s);
462
- if (!v || typeof v !== "object") return { state: {} };
463
- const o = v;
464
- const t = o[SEND_TIME_KEY];
465
- if (typeof t === "number" && Number.isFinite(t)) {
466
- const state = { ...o };
467
- delete state[SEND_TIME_KEY];
468
- return { state, sendTime: t };
469
- }
470
- return { state: o };
471
- } catch {
472
- return { state: {} };
473
- }
474
- }
475
- function parseValue(s) {
476
- if (s == null) return void 0;
477
- try {
478
- return JSON.parse(s);
479
- } catch {
480
- return void 0;
481
- }
482
- }
483
- var finiteReplacer = (_k, v) => {
484
- if (typeof v === "number") return Number.isFinite(v) ? v : 0;
485
- if (typeof v === "bigint") return Number(v);
486
- return v;
487
- };
488
- function sanitizeFinite(obj) {
489
- return JSON.parse(JSON.stringify(obj, finiteReplacer));
490
- }
491
- var nowMs = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
492
- function slerp(a, b, t) {
493
- let [bx, by, bz, bw] = b;
494
- const [ax, ay, az, aw] = a;
495
- let dot = ax * bx + ay * by + az * bz + aw * bw;
496
- if (dot < 0) {
497
- bx = -bx;
498
- by = -by;
499
- bz = -bz;
500
- bw = -bw;
501
- dot = -dot;
502
- }
503
- if (dot > 0.9995) {
504
- const x = ax + (bx - ax) * t, y = ay + (by - ay) * t, z = az + (bz - az) * t, w = aw + (bw - aw) * t;
505
- const l = Math.hypot(x, y, z, w) || 1;
506
- return [x / l, y / l, z / l, w / l];
507
- }
508
- const theta0 = Math.acos(dot);
509
- const sin0 = Math.sin(theta0);
510
- const s0 = Math.sin(theta0 - theta0 * t) / sin0;
511
- const s1 = Math.sin(theta0 * t) / sin0;
512
- return [ax * s0 + bx * s1, ay * s0 + by * s1, az * s0 + bz * s1, aw * s0 + bw * s1];
513
- }
514
- function isQuat(v) {
515
- return Array.isArray(v) && v.length === 4 && v.every((n) => typeof n === "number" && Number.isFinite(n));
516
- }
517
- function blend(a, b, tNum, tQuat) {
518
- const out = {};
519
- const interpolating = tNum < 1;
520
- for (const k in b) {
521
- const av = a[k];
522
- const bv = b[k];
523
- if (typeof av === "number" && typeof bv === "number" && Number.isFinite(av) && Number.isFinite(bv)) {
524
- out[k] = av + (bv - av) * tNum;
525
- } else if (isQuat(av) && isQuat(bv)) {
526
- out[k] = slerp(av, bv, tQuat);
527
- } else {
528
- out[k] = interpolating && k in a ? av : bv;
529
- }
530
- }
531
- return out;
532
- }
533
- function easeState(prev, target, aNum, aQuat) {
534
- const out = {};
535
- for (const k in target) {
536
- const tv = target[k];
537
- const pv = prev[k];
538
- if (typeof pv === "number" && typeof tv === "number" && Number.isFinite(pv) && Number.isFinite(tv)) {
539
- out[k] = pv + (tv - pv) * aNum;
540
- } else if (isQuat(pv) && isQuat(tv)) {
541
- out[k] = slerp(pv, tv, aQuat);
542
- } else {
543
- out[k] = tv;
544
- }
545
- }
546
- return out;
547
- }
548
- function sampleBuffer(buf, renderTime) {
549
- const n = buf.length;
550
- if (n === 0) return void 0;
551
- if (n === 1) return buf[0].state;
552
- let j = 0;
553
- while (j < n && buf[j].t <= renderTime) j++;
554
- if (j === 0) return buf[0].state;
555
- if (j >= n) {
556
- const a2 = buf[n - 2];
557
- const b2 = buf[n - 1];
558
- const span2 = b2.t - a2.t;
559
- if (span2 <= 0) return b2.state;
560
- const tPos = (Math.min(renderTime, b2.t + MAX_EXTRAP_MS) - a2.t) / span2;
561
- const tRot = (Math.min(renderTime, b2.t + ROT_EXTRAP_MS) - a2.t) / span2;
562
- return blend(a2.state, b2.state, tPos, tRot);
563
- }
564
- const a = buf[j - 1];
565
- const b = buf[j];
566
- const span = b.t - a.t;
567
- const t = span > 0 ? (renderTime - a.t) / span : 1;
568
- return blend(a.state, b.state, t, t);
569
- }
570
- function median(xs) {
571
- if (xs.length === 0) return 66;
572
- const s = [...xs].sort((a, b) => a - b);
573
- return s[s.length >> 1];
574
- }
575
- function quantile(xs, q) {
576
- if (xs.length === 0) return 0;
577
- const s = [...xs].sort((a, b) => a - b);
578
- const i = Math.min(s.length - 1, Math.max(0, Math.round(q * (s.length - 1))));
579
- return s[i];
580
- }
581
- async function connect(opts) {
582
- const client = await relayClient(opts);
583
- const room = await client.joinOrCreate(RELAY_ROOM_TYPE, {
584
- gameId: opts.room,
585
- name: opts.name,
586
- embedToken: resolveAuth(opts.auth)?.embedToken,
587
- ...opts.options,
588
- __v: MP_PROTOCOL
589
- // tells the relay this client hides reserved `__` keys (see MP_PROTOCOL)
590
- });
591
- return attachSession(client, room);
592
- }
593
- var RESERVED_SEND_TYPES = /* @__PURE__ */ new Set([
594
- "state",
595
- "shared",
596
- "claim",
597
- "obj",
598
- "release",
599
- "destroy",
600
- STATE_SNAP_MSG,
601
- OBJECT_SNAP_MSG,
602
- // Server→client control verbs a game must never ORIGINATE — they'd broadcast to peers via the
603
- // relay's `*` handler: 'server:restart' would trip every peer's save-flush, 'rate:drop' a bogus
604
- // over-budget warning, 'queue'/'seat' spurious matchmaking control.
605
- "server:restart",
606
- "rate:drop",
607
- "queue",
608
- "seat",
609
- CAPABILITIES_MSG,
610
- CAPABILITIES_REQUEST_MSG,
611
- OBJECT_CONTROL_RESULT_MSG
612
- ]);
613
- var isReservedSendType = (type) => RESERVED_SEND_TYPES.has(type) || type.startsWith("match:") || type.startsWith(RESERVED_PREFIX);
614
- var RECONNECT_DELAYS_MS = [500, 1e3, 2e3, 4e3, 8e3];
615
- var ROOM_REF = /* @__PURE__ */ Symbol("genex.roomRef");
616
- var roomOf = (s) => s[ROOM_REF]();
617
- function attachSession(client, initialRoom) {
618
- let room = initialRoom;
619
- const buffers = /* @__PURE__ */ new Map();
620
- const lastStamp = /* @__PURE__ */ new Map();
621
- const clockOffsets = /* @__PURE__ */ new Map();
622
- const offsetWin = /* @__PURE__ */ new Map();
623
- const smoothed = /* @__PURE__ */ new Map();
624
- const smoothedAt = /* @__PURE__ */ new Map();
625
- const intervalsById = /* @__PURE__ */ new Map();
626
- const autoDelayById = /* @__PURE__ */ new Map();
627
- let localState;
628
- const objOwners = /* @__PURE__ */ new Map();
629
- const objEpochs = /* @__PURE__ */ new Map();
630
- const objRaw = /* @__PURE__ */ new Map();
631
- const localObjState = /* @__PURE__ */ new Map();
632
- const warnedUnownedWrites = /* @__PURE__ */ new Set();
633
- const pendingBridge = /* @__PURE__ */ new Map();
634
- let hostId;
635
- let objectControlVersion = 0;
636
- let requestCounter = 0;
637
- const pendingControls = /* @__PURE__ */ new Map();
638
- const testSkew = typeof globalThis !== "undefined" && globalThis.__GENEX_MP_TEST_SKEW__ || 0;
639
- const now = testSkew ? () => nowMs() + testSkew : nowMs;
640
- const rejectUnownedWrite = (operation, id) => {
641
- if (objOwners.get(id) === room.sessionId) return false;
642
- const warningKey = `${operation}:${id}`;
643
- if (!warnedUnownedWrites.has(warningKey)) {
644
- warnedUnownedWrites.add(warningKey);
645
- console.warn(
646
- `[multiplayer] objects.${operation}('${id}') ignored because this session does not own the object \u2014 await objects.claimConfirmed('${id}') and check accepted before publishing`
647
- );
648
- }
649
- return true;
650
- };
651
- const pushSnap = (id, state, sendTime) => {
652
- const arrival = now();
653
- const t = sendTime ?? arrival;
654
- if (sendTime != null) {
655
- let win = offsetWin.get(id);
656
- if (!win) offsetWin.set(id, win = []);
657
- win.push(arrival - sendTime);
658
- if (win.length > CLOCK_WIN) win.shift();
659
- clockOffsets.set(id, Math.min(...win));
660
- }
661
- const prev = lastStamp.get(id);
662
- lastStamp.set(id, t);
663
- if (prev != null) {
664
- const gap = t - prev;
665
- if (gap > 5 && gap < 2e3) {
666
- let iv = intervalsById.get(id);
667
- if (!iv) intervalsById.set(id, iv = []);
668
- iv.push(gap);
669
- if (iv.length > 30) iv.shift();
670
- const ow = offsetWin.get(id);
671
- const jitter = ow && ow.length >= 8 ? quantile(ow, 0.9) - Math.min(...ow) : 0;
672
- const target = Math.min(
673
- INTERP_MAX_MS,
674
- Math.max(INTERP_MIN_MS, INTERP_RATIO * median(iv), JITTER_SAFETY * jitter)
675
- );
676
- const cur = autoDelayById.get(id) ?? 100;
677
- autoDelayById.set(id, cur + (target - cur) * INTERP_EASE);
678
- }
679
- }
680
- let buf = buffers.get(id);
681
- if (!buf) buffers.set(id, buf = []);
682
- const bridge = pendingBridge.get(id);
683
- if (bridge && buf.length === 0 && sendTime != null) {
684
- const delay = autoDelayById.get(id) ?? INTERP_MIN_MS;
685
- buf.push({ t: t - Math.max(1, delay), state: bridge });
686
- }
687
- pendingBridge.delete(id);
688
- buf.push({ t, state });
689
- const cutoff = t - 1500;
690
- while (buf.length > 2 && buf[0].t < cutoff) buf.shift();
691
- while (buf.length > 1 && buf[0].t > buf[buf.length - 1].t + POISON_GUARD_MS) buf.shift();
692
- };
693
- const sampleSmoothed = (streamKey, localNow) => {
694
- const buf = buffers.get(streamKey);
695
- const autoDelay = autoDelayById.get(streamKey) ?? 100;
696
- const renderTime = localNow - (clockOffsets.get(streamKey) ?? 0) - autoDelay;
697
- const sampled = buf && sampleBuffer(buf, renderTime);
698
- if (!sampled) return smoothed.get(streamKey);
699
- const prev = smoothed.get(streamKey);
700
- let out;
701
- if (prev) {
702
- const dt = Math.min(250, Math.max(0, localNow - (smoothedAt.get(streamKey) ?? localNow)));
703
- out = easeState(prev, sampled, 1 - Math.exp(-dt / SMOOTH_TAU_POS), 1 - Math.exp(-dt / SMOOTH_TAU_ROT));
704
- } else {
705
- out = sampled;
706
- }
707
- smoothed.set(streamKey, out);
708
- smoothedAt.set(streamKey, localNow);
709
- return out;
710
- };
711
- const clearStream = (streamKey) => {
712
- buffers.delete(streamKey);
713
- lastStamp.delete(streamKey);
714
- clockOffsets.delete(streamKey);
715
- offsetWin.delete(streamKey);
716
- smoothed.delete(streamKey);
717
- smoothedAt.delete(streamKey);
718
- intervalsById.delete(streamKey);
719
- autoDelayById.delete(streamKey);
720
- pendingBridge.delete(streamKey);
721
- };
722
- const clearStreamClock = (streamKey) => {
723
- buffers.delete(streamKey);
724
- lastStamp.delete(streamKey);
725
- clockOffsets.delete(streamKey);
726
- offsetWin.delete(streamKey);
727
- intervalsById.delete(streamKey);
728
- autoDelayById.delete(streamKey);
729
- };
730
- const emitters = /* @__PURE__ */ new Map();
731
- const bridged = /* @__PURE__ */ new Set();
732
- const reserved = /* @__PURE__ */ new Set([
733
- "join",
734
- "leave",
735
- "change",
736
- "shared",
737
- "object",
738
- "host",
739
- "reconnecting",
740
- "reconnected",
741
- "disconnect",
742
- // Locally emitted by the voice mesh — never relay message types. Without these here,
743
- // room.on('voice', …) would register a stray room.onMessage('voice') as well.
744
- "voice",
745
- "voice:rejected",
746
- INPUT_MSG,
747
- // inputs.on() has its own channel — never bridged as a custom event
748
- "rate:drop",
749
- // relay → us only (over-budget warning); handled internally, never bridged
750
- CAPABILITIES_MSG,
751
- CAPABILITIES_REQUEST_MSG,
752
- OBJECT_CONTROL_RESULT_MSG,
753
- STATE_SNAP_MSG,
754
- OBJECT_SNAP_MSG,
755
- // chat.on() has its own channel. Bridging these would re-register room.onMessage for
756
- // the same type and clobber the chat listener, silently killing chat for that game.
757
- CHAT_MSG,
758
- CHAT_HISTORY_MSG,
759
- CHAT_HISTORY_REQUEST_MSG,
760
- // voice.* owns these; bridging them would clobber the mesh's own listeners.
761
- VOICE_SIGNAL_MSG,
762
- VOICE_REJECTED_MSG,
763
- VOICE_JOIN_MSG,
764
- VOICE_LEAVE_MSG,
765
- VOICE_ICE_REQUEST_MSG,
766
- VOICE_ICE_MSG
767
- ]);
768
- const emit = (event, ...args) => emitters.get(event)?.forEach((cb) => cb(...args));
769
- const lastShared = /* @__PURE__ */ new Map();
770
- const handleShared = (value, key) => {
771
- if (lastShared.get(key) === value) return;
772
- lastShared.set(key, value);
773
- if (key === MATCH_KEY) return;
774
- if (key === HOST_KEY) {
775
- const next = parseValue(value) || void 0;
776
- if (next !== hostId) {
777
- hostId = next;
778
- emit("host", hostId);
779
- }
780
- return;
781
- }
782
- if (key === VOICE_KEY) {
783
- const ids = parseValue(value);
784
- voice.setRoster(Array.isArray(ids) ? ids.filter((v) => typeof v === "string") : []);
785
- emit("voice", voice.participants);
786
- return;
787
- }
788
- if (key.startsWith(OBJ_PREFIX)) {
789
- const id = key.slice(OBJ_PREFIX.length);
790
- const blob = parseValue(value);
791
- if (!blob || typeof blob !== "object") return;
792
- const owner = typeof blob.o === "string" && blob.o ? blob.o : void 0;
793
- const ownerChanged = owner !== objOwners.get(id);
794
- const epoch = typeof blob.e === "number" && Number.isFinite(blob.e) ? blob.e : 0;
795
- const epochChanged = objEpochs.has(id) && epoch !== objEpochs.get(id);
796
- objEpochs.set(id, epoch);
797
- if (epochChanged) clearStream(key);
798
- if (ownerChanged) {
799
- if (owner) objOwners.set(id, owner);
800
- else objOwners.delete(id);
801
- if (owner !== room.sessionId) {
802
- localObjState.delete(id);
803
- if (!epochChanged) clearStreamClock(key);
804
- }
805
- }
806
- if (blob.d && typeof blob.d === "object" && Object.keys(blob.d).length > 0) {
807
- objRaw.set(id, blob.d);
808
- if (owner !== room.sessionId) {
809
- if (epochChanged) {
810
- smoothed.set(key, blob.d);
811
- smoothedAt.set(key, now());
812
- pushSnap(key, blob.d, blob.t);
813
- } else if (ownerChanged) {
814
- pendingBridge.set(key, smoothed.get(key) ?? blob.d);
815
- } else {
816
- pushSnap(key, blob.d, blob.t);
817
- }
818
- }
819
- }
820
- if (ownerChanged) emit("object", id);
821
- return;
822
- }
823
- emit("shared", key, parseValue(value));
824
- };
825
- const parseCache = /* @__PURE__ */ new Map();
826
- const playerEpochs = /* @__PURE__ */ new Map();
827
- let lastPlayersView = /* @__PURE__ */ new Map();
828
- const cachedParse = (id, raw) => {
829
- if (!raw) return {};
830
- const hit = parseCache.get(id);
831
- if (hit && hit.raw === raw) return hit.state;
832
- const { state } = parseData(raw);
833
- parseCache.set(id, { raw, state });
834
- return state;
835
- };
836
- let intentionalLeave = false;
837
- let dead = false;
838
- let connected = true;
839
- let reconnectStartedAt = 0;
840
- const hostTickSyncs = /* @__PURE__ */ new Set();
841
- const syncHostTicks = () => hostTickSyncs.forEach((sync) => sync());
842
- const settleControl = (pending, result) => {
843
- pendingControls.delete(pending.requestId);
844
- if (pending.timeout) clearTimeout(pending.timeout);
845
- if (pending.capabilityTimer) clearTimeout(pending.capabilityTimer);
846
- pending.abort?.();
847
- if (pending.operation === "claim" && pending.optimistic && !result.accepted) {
848
- if (result.owner) objOwners.set(pending.id, result.owner);
849
- else objOwners.delete(pending.id);
850
- if (result.owner !== room.sessionId) localObjState.delete(pending.id);
851
- clearStreamClock(OBJ_PREFIX + pending.id);
852
- }
853
- if (pending.operation === "claim" && result.accepted) {
854
- objOwners.set(pending.id, room.sessionId);
855
- clearStreamClock(OBJ_PREFIX + pending.id);
856
- }
857
- if (pending.operation === "release" && result.accepted) {
858
- localObjState.delete(pending.id);
859
- if (objOwners.get(pending.id) === room.sessionId) objOwners.delete(pending.id);
860
- clearStreamClock(OBJ_PREFIX + pending.id);
861
- }
862
- if (pending.operation === "remove" && result.accepted) {
863
- localObjState.delete(pending.id);
864
- objRaw.delete(pending.id);
865
- objOwners.delete(pending.id);
866
- clearStream(OBJ_PREFIX + pending.id);
867
- }
868
- pending.resolve(result);
869
- };
870
- const settleAllControls = (reason) => {
871
- for (const pending of [...pendingControls.values()]) {
872
- settleControl(pending, {
873
- requestId: pending.requestId,
874
- operation: pending.operation,
875
- id: pending.id,
876
- accepted: false,
877
- owner: pending.optimistic ? pending.previousOwner : objOwners.get(pending.id),
878
- sequence: 0,
879
- reason
880
- });
881
- }
882
- };
883
- const sendPendingControl = (pending) => {
884
- if (!connected || !room.connection?.isOpen) return;
885
- send0(
886
- pending.operation === "remove" ? "destroy" : pending.operation,
887
- pending.payload
888
- );
889
- };
890
- const flushCapabilityWaiters = () => {
891
- if (objectControlVersion < OBJECT_CONTROL_VERSION) return;
892
- for (const pending of pendingControls.values()) {
893
- if (pending.capabilityTimer) {
894
- clearTimeout(pending.capabilityTimer);
895
- pending.capabilityTimer = null;
896
- }
897
- sendPendingControl(pending);
898
- }
899
- };
900
- const confirmedControl = (operation, id, options = {}) => {
901
- const key = String(id);
902
- const requestId = `${room.sessionId.slice(0, 12)}:${Date.now().toString(36)}:${(++requestCounter).toString(36)}`;
903
- const timeoutMs = Math.max(250, Math.min(options.timeoutMs ?? 5e3, 6e4));
904
- return new Promise((resolve) => {
905
- const pending = {
906
- operation,
907
- id: key,
908
- requestId,
909
- payload: {
910
- id: key,
911
- requestId,
912
- ...options.authority === "host" ? { authority: "host" } : {}
913
- },
914
- optimistic: operation === "claim" && options.optimistic === true,
915
- previousOwner: objOwners.get(key),
916
- resolve,
917
- timeout: null,
918
- capabilityTimer: null,
919
- abort: void 0
920
- };
921
- if (pending.optimistic) objOwners.set(key, room.sessionId);
922
- pending.timeout = setTimeout(() => {
923
- settleControl(pending, {
924
- requestId,
925
- operation,
926
- id: key,
927
- accepted: false,
928
- owner: pending.optimistic ? pending.previousOwner : objOwners.get(key),
929
- sequence: 0,
930
- reason: "timeout"
931
- });
932
- }, timeoutMs);
933
- if (options.signal) {
934
- const onAbort = () => settleControl(pending, {
935
- requestId,
936
- operation,
937
- id: key,
938
- accepted: false,
939
- owner: pending.optimistic ? pending.previousOwner : objOwners.get(key),
940
- sequence: 0,
941
- reason: "aborted"
942
- });
943
- options.signal.addEventListener("abort", onAbort, { once: true });
944
- pending.abort = () => options.signal?.removeEventListener("abort", onAbort);
945
- }
946
- pendingControls.set(requestId, pending);
947
- if (options.signal?.aborted) {
948
- settleControl(pending, {
949
- requestId,
950
- operation,
951
- id: key,
952
- accepted: false,
953
- owner: pending.optimistic ? pending.previousOwner : objOwners.get(key),
954
- sequence: 0,
955
- reason: "aborted"
956
- });
957
- return;
958
- }
959
- if (objectControlVersion >= OBJECT_CONTROL_VERSION) {
960
- sendPendingControl(pending);
961
- } else {
962
- pending.capabilityTimer = setTimeout(() => {
963
- if (!pendingControls.has(requestId) || objectControlVersion >= OBJECT_CONTROL_VERSION) return;
964
- settleControl(pending, {
965
- requestId,
966
- operation,
967
- id: key,
968
- accepted: false,
969
- owner: pending.optimistic ? pending.previousOwner : objOwners.get(key),
970
- sequence: 0,
971
- reason: "unsupported"
972
- });
973
- }, 500);
974
- }
975
- });
976
- };
977
- let droppedWhileOffline = 0;
978
- const send0 = (type, payload, countIfDropped = false) => {
979
- if (room.connection?.isOpen) {
980
- try {
981
- room.send(type, payload);
982
- return;
983
- } catch {
984
- }
985
- }
986
- if (countIfDropped) droppedWhileOffline++;
987
- };
988
- const voice = new VoiceMesh({
989
- selfId: () => room.sessionId,
990
- nameOf: (id) => room.state?.players?.get(id)?.name ?? "",
991
- sendSignal: (to, data) => send0(VOICE_SIGNAL_MSG, { to, data }, true)
992
- });
993
- voice.onChange(() => emit("voice", voice.participants));
994
- let iceRefreshTimer = null;
995
- const ice = createIceRequester(() => send0(VOICE_ICE_REQUEST_MSG, {}, true), ICE_REQUEST_TIMEOUT_MS);
996
- const clearIceRefresh = () => {
997
- if (iceRefreshTimer) clearTimeout(iceRefreshTimer);
998
- iceRefreshTimer = null;
999
- };
1000
- const scheduleIceRefresh = (ttl) => {
1001
- clearIceRefresh();
1002
- if (typeof ttl !== "number" || !Number.isFinite(ttl) || ttl <= 0) return;
1003
- iceRefreshTimer = setTimeout(() => {
1004
- if (!voice.active) return;
1005
- void ice.request().then((cfg) => {
1006
- if (!voice.active || !cfg?.iceServers?.length) return;
1007
- voice.setIceServers(cfg.iceServers);
1008
- scheduleIceRefresh(cfg.ttl);
1009
- });
1010
- }, Math.max(6e4, Math.floor(ttl * 800)));
1011
- };
1012
- const chatHistory = [];
1013
- const chatSeen = /* @__PURE__ */ new Set();
1014
- const chatListeners = /* @__PURE__ */ new Set();
1015
- const chatMuted = /* @__PURE__ */ new Set();
1016
- const isChatMessage = (m) => !!m && typeof m === "object" && typeof m.id === "number" && typeof m.from === "string" && typeof m.name === "string" && typeof m.text === "string";
1017
- const acceptChat = (message) => {
1018
- if (!isChatMessage(message)) return;
1019
- if (chatSeen.has(message.id)) return;
1020
- if (chatMuted.has(message.from)) return;
1021
- chatSeen.add(message.id);
1022
- const last = chatHistory[chatHistory.length - 1];
1023
- if (!last || message.id > last.id) chatHistory.push(message);
1024
- else {
1025
- let i = chatHistory.length;
1026
- while (i > 0 && (chatHistory[i - 1]?.id ?? 0) > message.id) i--;
1027
- chatHistory.splice(i, 0, message);
1028
- }
1029
- for (const cb of [...chatListeners]) {
1030
- try {
1031
- cb(message);
1032
- } catch (err) {
1033
- console.error("[multiplayer] a chat listener threw", err);
1034
- }
1035
- }
1036
- };
1037
- const wireRoom = (r) => {
1038
- let hydrated;
1039
- const $ = getStateCallbacks(r);
1040
- $(r.state).players.onAdd((player, id) => {
1041
- const { state, sendTime } = parseData(player.data);
1042
- const addEpoch = Number.isFinite(player.epoch) ? player.epoch : 0;
1043
- const addEpochChanged = playerEpochs.has(id) && addEpoch !== playerEpochs.get(id);
1044
- playerEpochs.set(id, addEpoch);
1045
- if (id !== room.sessionId && addEpochChanged) clearStream(id);
1046
- emit("join", id, state);
1047
- if (id !== room.sessionId && sendTime != null) pushSnap(id, state, sendTime);
1048
- $(player).onChange(() => {
1049
- const { state: st, sendTime: ts } = parseData(player.data);
1050
- const epoch = Number.isFinite(player.epoch) ? player.epoch : 0;
1051
- const epochChanged = epoch !== (playerEpochs.get(id) ?? epoch);
1052
- playerEpochs.set(id, epoch);
1053
- if (id !== room.sessionId && epochChanged) clearStream(id);
1054
- emit("change", id, st);
1055
- if (id !== room.sessionId) pushSnap(id, st, ts);
1056
- });
1057
- });
1058
- $(r.state).players.onRemove((_player, id) => {
1059
- emit("leave", id);
1060
- clearStream(id);
1061
- parseCache.delete(id);
1062
- playerEpochs.delete(id);
1063
- });
1064
- $(r.state).shared.onAdd(handleShared);
1065
- $(r.state).shared.onChange(handleShared);
1066
- $(r.state).shared.onRemove((_value, key) => {
1067
- lastShared.delete(key);
1068
- if (key === HOST_KEY) {
1069
- if (hostId !== void 0) {
1070
- hostId = void 0;
1071
- emit("host", void 0);
1072
- }
1073
- return;
1074
- }
1075
- if (key.startsWith(OBJ_PREFIX)) {
1076
- const id = key.slice(OBJ_PREFIX.length);
1077
- clearStream(key);
1078
- objRaw.delete(id);
1079
- objOwners.delete(id);
1080
- objEpochs.delete(id);
1081
- localObjState.delete(id);
1082
- emit("object", id);
1083
- }
1084
- });
1085
- const hydrateShared = (state) => {
1086
- const shared = state?.shared;
1087
- shared?.forEach((value, key) => handleShared(value, key));
1088
- if (!shared) return;
1089
- for (const id of /* @__PURE__ */ new Set([...objOwners.keys(), ...objRaw.keys(), ...localObjState.keys()])) {
1090
- const key = OBJ_PREFIX + id;
1091
- if (shared.has(key)) continue;
1092
- lastShared.delete(key);
1093
- clearStream(key);
1094
- objRaw.delete(id);
1095
- objOwners.delete(id);
1096
- objEpochs.delete(id);
1097
- localObjState.delete(id);
1098
- emit("object", id);
1099
- }
1100
- if (hostId !== void 0 && !shared.has(HOST_KEY)) {
1101
- lastShared.delete(HOST_KEY);
1102
- hostId = void 0;
1103
- emit("host", void 0);
1104
- }
1105
- };
1106
- if (r.state?.shared) {
1107
- hydrateShared(r.state);
1108
- hydrated = Promise.resolve();
1109
- } else {
1110
- hydrated = new Promise((resolve) => {
1111
- r.onStateChange.once((state) => {
1112
- hydrateShared(state);
1113
- resolve();
1114
- });
1115
- });
1116
- }
1117
- r.onMessage(CAPABILITIES_MSG, (msg) => {
1118
- if (typeof msg?.objectControl === "number") {
1119
- objectControlVersion = Math.max(objectControlVersion, msg.objectControl);
1120
- flushCapabilityWaiters();
1121
- }
1122
- });
1123
- r.onMessage(CAPABILITIES_REQUEST_MSG, () => {
1124
- });
1125
- send0(CAPABILITIES_REQUEST_MSG, { objectControl: OBJECT_CONTROL_VERSION });
1126
- r.onMessage(VOICE_SIGNAL_MSG, (msg) => {
1127
- if (msg && typeof msg.from === "string") void voice.handleSignal(msg.from, msg.data);
1128
- });
1129
- r.onMessage(VOICE_REJECTED_MSG, (msg) => {
1130
- clearIceRefresh();
1131
- voice.stop();
1132
- emit("voice:rejected", msg ?? { reason: "full" });
1133
- });
1134
- r.onMessage(VOICE_ICE_MSG, (msg) => {
1135
- ice.deliver(msg ?? null);
1136
- });
1137
- for (const inert of [
1138
- VOICE_JOIN_MSG,
1139
- VOICE_LEAVE_MSG,
1140
- VOICE_ICE_REQUEST_MSG,
1141
- CHAT_HISTORY_REQUEST_MSG
1142
- ]) {
1143
- r.onMessage(inert, () => {
1144
- });
1145
- }
1146
- r.onMessage(CHAT_MSG, (msg) => acceptChat(msg));
1147
- r.onMessage(CHAT_HISTORY_MSG, (msg) => {
1148
- if (!Array.isArray(msg?.messages)) return;
1149
- for (const m of msg.messages) acceptChat(m);
1150
- });
1151
- send0(CHAT_HISTORY_REQUEST_MSG, {});
1152
- r.onMessage(OBJECT_CONTROL_RESULT_MSG, (msg) => {
1153
- if (!msg || typeof msg.requestId !== "string") return;
1154
- const pending = pendingControls.get(msg.requestId);
1155
- if (!pending || msg.operation !== pending.operation || msg.id !== pending.id) return;
1156
- settleControl(pending, msg);
1157
- });
1158
- r.onMessage(INPUT_MSG, (msg) => {
1159
- if (msg && typeof msg.from === "string") emit(INPUT_MSG, msg.from, msg.d);
1160
- });
1161
- r.onMessage("rate:drop", (msg) => {
1162
- console.warn(
1163
- `[multiplayer] the relay dropped ${msg?.dropped ?? "some"} of your messages in the last second \u2014 you are publishing over budget. Publish idle/unchanged objects at \u22642Hz keepalive, not every tick (see the multiplayer skill's "message budget" section).`
1164
- );
1165
- });
1166
- for (const event of bridged) r.onMessage(event, (msg) => emit(event, msg));
1167
- r.onLeave((code) => handleLeave(r, code ?? 1006));
1168
- r.onError(() => {
1169
- });
1170
- return hydrated;
1171
- };
1172
- const handleLeave = (left, code) => {
1173
- if (left !== room || dead) return;
1174
- connected = false;
1175
- reconnectStartedAt ||= nowMs();
1176
- syncHostTicks();
1177
- if (intentionalLeave) return;
1178
- if (code === 1e3 || code >= 4e3) {
1179
- dead = true;
1180
- settleAllControls("disconnected");
1181
- emit("disconnect", code);
1182
- return;
1183
- }
1184
- void attemptReconnect(left.reconnectionToken, code);
1185
- };
1186
- const attemptReconnect = async (token, code) => {
1187
- for (let attempt = 1; attempt <= RECONNECT_DELAYS_MS.length + 1; attempt++) {
1188
- emit("reconnecting", { attempt, code });
1189
- if (intentionalLeave || dead) return;
1190
- try {
1191
- const next = await client.reconnect(token);
1192
- room = next;
1193
- const reconnectGap = reconnectStartedAt ? nowMs() - reconnectStartedAt : 0;
1194
- reconnectStartedAt = 0;
1195
- if (reconnectGap > 1e3) {
1196
- for (const streamKey of [...buffers.keys()]) clearStream(streamKey);
1197
- }
1198
- const hydrated = wireRoom(next);
1199
- await Promise.race([hydrated, new Promise((r) => setTimeout(r, 2e3))]);
1200
- if (room !== next || dead || intentionalLeave) return;
1201
- connected = true;
1202
- syncHostTicks();
1203
- for (const pending of pendingControls.values()) sendPendingControl(pending);
1204
- emit("reconnected");
1205
- if (droppedWhileOffline > 0) {
1206
- console.warn(
1207
- `[multiplayer] ${droppedWhileOffline} fire-and-forget event(s)/input(s) were dropped while reconnecting \u2014 they are not buffered; player/object state re-synced automatically.`
1208
- );
1209
- droppedWhileOffline = 0;
1210
- }
1211
- return;
1212
- } catch {
1213
- }
1214
- if (attempt <= RECONNECT_DELAYS_MS.length) {
1215
- await new Promise((r) => setTimeout(r, RECONNECT_DELAYS_MS[attempt - 1]));
1216
- if (intentionalLeave || dead) return;
1217
- }
1218
- }
1219
- dead = true;
1220
- settleAllControls("disconnected");
1221
- emit("disconnect", code);
1222
- };
1223
- wireRoom(room);
1224
- const on = ((event, cb) => {
1225
- let set = emitters.get(event);
1226
- if (!set) emitters.set(event, set = /* @__PURE__ */ new Set());
1227
- set.add(cb);
1228
- if (!reserved.has(event) && !bridged.has(event)) {
1229
- bridged.add(event);
1230
- room.onMessage(event, (msg) => emit(event, msg));
1231
- }
1232
- return () => {
1233
- set?.delete(cb);
1234
- };
1235
- });
1236
- const session = {
1237
- id: room.sessionId,
1238
- get players() {
1239
- const out = /* @__PURE__ */ new Map();
1240
- const localNow = now();
1241
- room.state?.players?.forEach((p, id) => {
1242
- const latest = cachedParse(id, p.data);
1243
- let state = latest;
1244
- let raw = latest;
1245
- if (id === room.sessionId) {
1246
- if (localState) {
1247
- state = localState;
1248
- raw = localState;
1249
- }
1250
- } else {
1251
- state = sampleSmoothed(id, localNow) ?? latest;
1252
- }
1253
- out.set(id, {
1254
- id,
1255
- name: p.name,
1256
- avatarUrl: p.avatarUrl ?? "",
1257
- // old relay schemas did not carry the field
1258
- connected: p.connected !== false,
1259
- // old relay schemas did not carry the field
1260
- state,
1261
- stateRaw: raw
1262
- });
1263
- });
1264
- if (!connected && out.size === 0 && lastPlayersView.size > 0) return new Map(lastPlayersView);
1265
- lastPlayersView = out;
1266
- return out;
1267
- },
1268
- get activePlayers() {
1269
- return new Map([...session.players].filter(([, player]) => player.connected));
1270
- },
1271
- me: {
1272
- set(state) {
1273
- if (SEND_TIME_KEY in state) {
1274
- console.warn(`[multiplayer] '${SEND_TIME_KEY}' is a reserved field name and will be overwritten by the send timestamp \u2014 rename it.`);
1275
- }
1276
- const clean = sanitizeFinite(state);
1277
- localState = clean;
1278
- send0("state", JSON.stringify({ ...clean, [SEND_TIME_KEY]: now() }));
1279
- },
1280
- snap(state) {
1281
- if (SEND_TIME_KEY in state) {
1282
- console.warn(`[multiplayer] '${SEND_TIME_KEY}' is a reserved field name and will be overwritten by the send timestamp \u2014 rename it.`);
1283
- }
1284
- const clean = sanitizeFinite(state);
1285
- localState = clean;
1286
- send0(STATE_SNAP_MSG, JSON.stringify({ ...clean, [SEND_TIME_KEY]: now() }));
1287
- }
1288
- },
1289
- shared: {
1290
- get(key) {
1291
- if (key.startsWith(RESERVED_PREFIX)) return void 0;
1292
- return parseValue(room.state?.shared?.get(key));
1293
- },
1294
- set(key, value) {
1295
- if (key.startsWith(RESERVED_PREFIX)) return;
1296
- send0("shared", { key, value: JSON.stringify(sanitizeFinite(value)) });
1297
- },
1298
- keys() {
1299
- return room.state?.shared ? Array.from(room.state.shared.keys()).filter((k) => !k.startsWith(RESERVED_PREFIX)) : [];
1300
- }
1301
- },
1302
- objects: {
1303
- claim(id) {
1304
- const key = String(id);
1305
- objOwners.set(key, room.sessionId);
1306
- send0("claim", { id: key });
1307
- },
1308
- claimConfirmed(id, options) {
1309
- return confirmedControl("claim", id, options);
1310
- },
1311
- release(id) {
1312
- const key = String(id);
1313
- localObjState.delete(key);
1314
- if (objOwners.get(key) === room.sessionId) objOwners.delete(key);
1315
- clearStreamClock(OBJ_PREFIX + key);
1316
- send0("release", { id: key });
1317
- },
1318
- releaseConfirmed(id, options) {
1319
- return confirmedControl("release", id, options);
1320
- },
1321
- remove(id) {
1322
- const key = String(id);
1323
- if (objOwners.get(key) === room.sessionId) {
1324
- localObjState.delete(key);
1325
- objRaw.delete(key);
1326
- objOwners.delete(key);
1327
- clearStream(OBJ_PREFIX + key);
1328
- }
1329
- send0("destroy", { id: key });
1330
- },
1331
- removeConfirmed(id, options) {
1332
- return confirmedControl("remove", id, options);
1333
- },
1334
- set(id, state) {
1335
- const key = String(id);
1336
- if (rejectUnownedWrite("set", key)) return;
1337
- const clean = sanitizeFinite(state);
1338
- if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
1339
- send0("obj", { id: key, d: clean, t: now() });
1340
- },
1341
- snap(id, state) {
1342
- const key = String(id);
1343
- if (rejectUnownedWrite("snap", key)) return;
1344
- const clean = sanitizeFinite(state);
1345
- if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
1346
- send0(OBJECT_SNAP_MSG, { id: key, d: clean, t: now() });
1347
- },
1348
- get(id) {
1349
- const key = String(id);
1350
- const owner = objOwners.get(key);
1351
- const mine = localObjState.get(key);
1352
- if (owner === void 0 && !objRaw.has(key) && !mine) return void 0;
1353
- const isMine = owner !== void 0 && owner === room.sessionId;
1354
- let state;
1355
- let raw;
1356
- if (isMine && mine) {
1357
- state = mine;
1358
- raw = mine;
1359
- smoothed.set(OBJ_PREFIX + key, mine);
1360
- smoothedAt.set(OBJ_PREFIX + key, now());
1361
- } else {
1362
- raw = objRaw.get(key) ?? {};
1363
- state = sampleSmoothed(OBJ_PREFIX + key, now()) ?? raw;
1364
- }
1365
- return {
1366
- id: key,
1367
- epoch: objEpochs.get(key) ?? 0,
1368
- owner,
1369
- isMine,
1370
- state,
1371
- stateRaw: raw
1372
- };
1373
- },
1374
- ids() {
1375
- return [.../* @__PURE__ */ new Set([...objOwners.keys(), ...objRaw.keys(), ...localObjState.keys()])];
1376
- }
1377
- },
1378
- get isHost() {
1379
- return hostId !== void 0 && hostId === room.sessionId;
1380
- },
1381
- get host() {
1382
- return hostId;
1383
- },
1384
- inputs: {
1385
- send(payload) {
1386
- send0(INPUT_MSG, sanitizeFinite(payload), true);
1387
- },
1388
- on(cb) {
1389
- let set = emitters.get(INPUT_MSG);
1390
- if (!set) emitters.set(INPUT_MSG, set = /* @__PURE__ */ new Set());
1391
- const listener = (from, payload) => cb(from, payload);
1392
- set.add(listener);
1393
- return () => void set?.delete(listener);
1394
- }
1395
- },
1396
- onHostTick(hz, cb) {
1397
- let timer = null;
1398
- let last = 0;
1399
- const safeHz = Number.isFinite(hz) && hz > 0 ? Math.min(hz, 100) : 20;
1400
- const intervalMs = Math.max(10, Math.round(1e3 / safeHz));
1401
- const sync = () => {
1402
- const shouldRun = connected && !intentionalLeave && !dead && hostId !== void 0 && hostId === room.sessionId;
1403
- if (shouldRun && !timer) {
1404
- last = nowMs();
1405
- timer = setInterval(() => {
1406
- const t = nowMs();
1407
- cb(Math.min(t - last, 250));
1408
- last = t;
1409
- }, intervalMs);
1410
- } else if (!shouldRun && timer) {
1411
- clearInterval(timer);
1412
- timer = null;
1413
- }
1414
- };
1415
- const off = on("host", sync);
1416
- hostTickSyncs.add(sync);
1417
- sync();
1418
- return () => {
1419
- off();
1420
- hostTickSyncs.delete(sync);
1421
- if (timer) clearInterval(timer);
1422
- timer = null;
1423
- };
1424
- },
1425
- on,
1426
- send(type, payload) {
1427
- if (isReservedSendType(type)) {
1428
- console.warn(`[multiplayer] send('${type}') collides with a relay-internal channel \u2014 use a different event name`);
1429
- return;
1430
- }
1431
- send0(type, payload, true);
1432
- },
1433
- chat: {
1434
- send(text) {
1435
- if (typeof text !== "string") return;
1436
- if (!text.trim()) return;
1437
- send0(CHAT_MSG, { text }, true);
1438
- },
1439
- on(cb) {
1440
- chatListeners.add(cb);
1441
- return () => chatListeners.delete(cb);
1442
- },
1443
- get history() {
1444
- return [...chatHistory];
1445
- },
1446
- mute(playerId) {
1447
- if (typeof playerId === "string" && playerId) chatMuted.add(playerId);
1448
- },
1449
- unmute(playerId) {
1450
- chatMuted.delete(playerId);
1451
- },
1452
- get muted() {
1453
- return [...chatMuted];
1454
- }
1455
- },
1456
- voice: {
1457
- async join(options) {
1458
- await voice.start(options, true);
1459
- if (!options?.iceServers?.length) {
1460
- const cfg = await ice.request();
1461
- if (voice.active && cfg?.iceServers?.length) {
1462
- voice.setIceServers(cfg.iceServers);
1463
- scheduleIceRefresh(cfg.ttl);
1464
- }
1465
- }
1466
- if (!voice.active) return;
1467
- send0(VOICE_JOIN_MSG, {}, true);
1468
- voice.releasePeering();
1469
- },
1470
- leave() {
1471
- clearIceRefresh();
1472
- send0(VOICE_LEAVE_MSG, {}, true);
1473
- voice.stop();
1474
- },
1475
- get active() {
1476
- return voice.active;
1477
- },
1478
- get participants() {
1479
- return voice.participants;
1480
- },
1481
- setMicEnabled(on2) {
1482
- voice.setMicEnabled(on2 === true);
1483
- },
1484
- get micEnabled() {
1485
- return voice.micEnabled;
1486
- },
1487
- get speaking() {
1488
- return voice.speaking;
1489
- },
1490
- mute(playerId) {
1491
- voice.mute(playerId);
1492
- },
1493
- unmute(playerId) {
1494
- voice.unmute(playerId);
1495
- },
1496
- setVolume(playerId, volume) {
1497
- voice.setVolume(playerId, volume);
1498
- },
1499
- get muted() {
1500
- return voice.participants.filter((p) => p.muted).map((p) => p.id);
1501
- }
1502
- },
1503
- setRoomOpen(open) {
1504
- room.send(ROOM_OPEN_MSG, { open });
1505
- },
1506
- leave() {
1507
- intentionalLeave = true;
1508
- voice.stop();
1509
- connected = false;
1510
- syncHostTicks();
1511
- settleAllControls("aborted");
1512
- void room.leave();
1513
- }
1514
- };
1515
- Object.defineProperty(session, ROOM_REF, { value: () => room, enumerable: false });
1516
- return session;
1517
- }
1518
- var SEARCH_BACKOFF_MS = [1e3, 2e3, 4e3];
1519
- var matchJoinOptions = (opts) => ({
1520
- gameId: opts.room,
1521
- name: opts.name,
1522
- embedToken: resolveAuth(opts.auth)?.embedToken,
1523
- __v: MP_PROTOCOL
1524
- });
1525
- function buildView(matchRoom, queueInfo) {
1526
- const raw = matchRoom?.state?.shared?.get(MATCH_KEY);
1527
- const parsed = raw != null ? parseValue(raw) : null;
1528
- const m = parsed && typeof parsed === "object" ? parsed : null;
1529
- if (!m) {
1530
- return {
1531
- status: matchRoom ? "waiting" : "searching",
1532
- role: "player",
1533
- queue: queueInfo,
1534
- players: [],
1535
- connectedPlayers: [],
1536
- opponents: [],
1537
- teams: {},
1538
- myTeam: null,
1539
- scores: {},
1540
- config: {},
1541
- winCondition: "lastStanding",
1542
- championId: null,
1543
- challengerId: null,
1544
- roundId: 0,
1545
- winnerId: null,
1546
- message: matchRoom ? "Waiting for players\u2026" : "Finding a match\u2026"
1547
- };
1548
- }
1549
- const myId = matchRoom?.sessionId;
1550
- const championId = m.championId ?? null;
1551
- const challengerId = m.challengerId ?? null;
1552
- const teams = m.teams ?? {};
1553
- const players = m.players ?? [championId, challengerId].filter((x) => !!x);
1554
- const connectedPlayers = m.connectedPlayers ?? players;
1555
- const isPlayer = myId != null && players.includes(myId);
1556
- const status = isPlayer ? m.phase : "spectating";
1557
- const opponents = players.filter((id) => id !== myId);
1558
- const myTeam = myId ? Object.keys(teams).find((t) => teams[t]?.includes(myId)) ?? null : null;
1559
- const { winCondition: wc, ...numericCfg } = m.cfg ?? {};
1560
- return {
1561
- status,
1562
- role: isPlayer ? "player" : "spectator",
1563
- queue: { position: 0, size: 0 },
1564
- players,
1565
- connectedPlayers,
1566
- opponents,
1567
- teams,
1568
- myTeam,
1569
- scores: m.scores ?? {},
1570
- config: numericCfg,
1571
- winCondition: wc?.type ?? "lastStanding",
1572
- championId,
1573
- challengerId,
1574
- roundId: m.roundId ?? 0,
1575
- winnerId: m.winnerId ?? null,
1576
- message: m.message ?? ""
1577
- };
1578
- }
1579
- async function matchmake(opts) {
1580
- const client = await relayClient(opts);
1581
- const emitters = /* @__PURE__ */ new Map();
1582
- const emit = (event, ...args) => emitters.get(event)?.forEach((cb) => cb(...args));
1583
- const on = ((event, cb) => {
1584
- let set = emitters.get(event);
1585
- if (!set) emitters.set(event, set = /* @__PURE__ */ new Set());
1586
- set.add(cb);
1587
- return () => void set?.delete(cb);
1588
- });
1589
- let session = null;
1590
- let matchRoom = null;
1591
- let queueRoom = null;
1592
- let queueInfo = { position: 0, size: 0 };
1593
- let cancelled = false;
1594
- let searching = false;
1595
- let lifecycleEpoch = 0;
1596
- let seating = false;
1597
- let requeueing = false;
1598
- const search = async () => {
1599
- if (cancelled || searching) return;
1600
- searching = true;
1601
- const epoch = ++lifecycleEpoch;
1602
- try {
1603
- session = null;
1604
- matchRoom = null;
1605
- for (let attempt = 0; ; attempt++) {
1606
- if (queueRoom) {
1607
- void queueRoom.leave();
1608
- queueRoom = null;
1609
- }
1610
- queueInfo = { position: 0, size: 0 };
1611
- let q;
1612
- try {
1613
- q = await client.joinOrCreate(QUEUE_ROOM_TYPE, matchJoinOptions(opts));
1614
- } catch (err) {
1615
- if (cancelled) return;
1616
- const delay = SEARCH_BACKOFF_MS[attempt];
1617
- if (delay == null) {
1618
- emit("error", err);
1619
- return;
1620
- }
1621
- await new Promise((r) => setTimeout(r, delay));
1622
- if (cancelled) return;
1623
- continue;
1624
- }
1625
- if (cancelled || epoch !== lifecycleEpoch) {
1626
- void q.leave();
1627
- return;
1628
- }
1629
- queueRoom = q;
1630
- q.onMessage("queue", (msg) => {
1631
- queueInfo = { position: msg.position, size: msg.size };
1632
- emit("queue", msg);
1633
- });
1634
- q.onMessage("seat", (msg) => {
1635
- if (cancelled || epoch !== lifecycleEpoch || queueRoom !== q || seating) return;
1636
- seating = true;
1637
- queueRoom = null;
1638
- void q.leave();
1639
- void consumeSeat(msg.reservation, msg.role, epoch);
1640
- });
1641
- q.onLeave(() => {
1642
- if (cancelled || queueRoom !== q) return;
1643
- queueRoom = null;
1644
- void (async () => {
1645
- for (const delayMs of RECONNECT_DELAYS_MS) {
1646
- await new Promise((r) => setTimeout(r, delayMs));
1647
- if (cancelled || queueRoom || session) return;
1648
- await search();
1649
- if (queueRoom || session) return;
1650
- }
1651
- })();
1652
- });
1653
- emit("searching");
1654
- return;
1655
- }
1656
- } finally {
1657
- searching = false;
1658
- }
1659
- };
1660
- const consumeSeat = async (reservation, role, epoch) => {
1661
- try {
1662
- const mr = await client.consumeSeatReservation(reservation);
1663
- if (cancelled || epoch !== lifecycleEpoch) {
1664
- void mr.leave();
1665
- return;
1666
- }
1667
- matchRoom = mr;
1668
- const s = attachSession(client, mr);
1669
- session = s;
1670
- s.on("match:start", (m) => emit("matchStart", m));
1671
- s.on("match:ended", (m) => emit("matchEnded", m));
1672
- s.on("match:requeue", () => {
1673
- if (session === s) void requeue();
1674
- });
1675
- s.on("disconnect", () => {
1676
- if (!cancelled && session === s) void requeue();
1677
- });
1678
- if (queueRoom) {
1679
- void queueRoom.leave();
1680
- queueRoom = null;
1681
- }
1682
- emit("matched");
1683
- if (role === "spectator") emit("spectate");
1684
- } catch {
1685
- if (!cancelled && epoch === lifecycleEpoch) {
1686
- seating = false;
1687
- void search();
1688
- }
1689
- } finally {
1690
- if (epoch === lifecycleEpoch) seating = false;
1691
- }
1692
- };
1693
- const requeue = async () => {
1694
- if (cancelled || requeueing) return;
1695
- requeueing = true;
1696
- try {
1697
- lifecycleEpoch++;
1698
- seating = false;
1699
- session?.leave();
1700
- matchRoom = null;
1701
- session = null;
1702
- await search();
1703
- } finally {
1704
- requeueing = false;
1705
- }
1706
- };
1707
- void search();
1708
- const liveRoom = () => session ? roomOf(session) : null;
1709
- let viewSession = null;
1710
- let lastHydratedView = null;
1711
- const stableView = () => {
1712
- if (session !== viewSession) {
1713
- viewSession = session;
1714
- lastHydratedView = null;
1715
- }
1716
- const live = liveRoom();
1717
- const hydrated = live?.state?.shared?.get(MATCH_KEY) != null;
1718
- if (!hydrated && session && lastHydratedView) return lastHydratedView;
1719
- const next = buildView(live, queueInfo);
1720
- if (hydrated) lastHydratedView = next;
1721
- return next;
1722
- };
1723
- return {
1724
- get session() {
1725
- return session;
1726
- },
1727
- get matchmaking() {
1728
- return stableView();
1729
- },
1730
- on,
1731
- eliminated() {
1732
- liveRoom()?.send("match:eliminated", {});
1733
- },
1734
- score(delta = 1) {
1735
- liveRoom()?.send("match:score", { delta });
1736
- },
1737
- finish() {
1738
- liveRoom()?.send("match:finish", {});
1739
- },
1740
- retry() {
1741
- if (cancelled || searching || queueRoom || matchRoom) return;
1742
- void search();
1743
- },
1744
- cancel() {
1745
- cancelled = true;
1746
- lifecycleEpoch++;
1747
- seating = false;
1748
- session?.leave();
1749
- if (!session && matchRoom) void matchRoom.leave();
1750
- if (queueRoom) void queueRoom.leave();
1751
- matchRoom = null;
1752
- queueRoom = null;
1753
- session = null;
1754
- }
1755
- };
1756
- }
1757
- function wirePrivate(client, room) {
1758
- const emitters = /* @__PURE__ */ new Map();
1759
- const emit = (event, ...args) => emitters.get(event)?.forEach((cb) => cb(...args));
1760
- const on = ((event, cb) => {
1761
- let set = emitters.get(event);
1762
- if (!set) emitters.set(event, set = /* @__PURE__ */ new Set());
1763
- set.add(cb);
1764
- return () => void set?.delete(cb);
1765
- });
1766
- let live = true;
1767
- const session = attachSession(client, room);
1768
- session.on("match:start", (m) => emit("matchStart", m));
1769
- session.on("match:ended", (m) => emit("matchEnded", m));
1770
- session.on("match:requeue", () => void (live = false));
1771
- session.on("disconnect", () => void (live = false));
1772
- const liveRoom = () => live ? roomOf(session) : null;
1773
- return {
1774
- get session() {
1775
- return live ? session : null;
1776
- },
1777
- get matchmaking() {
1778
- return buildView(liveRoom(), { position: 0, size: 0 });
1779
- },
1780
- on,
1781
- eliminated() {
1782
- liveRoom()?.send("match:eliminated", {});
1783
- },
1784
- score(delta = 1) {
1785
- liveRoom()?.send("match:score", { delta });
1786
- },
1787
- finish() {
1788
- liveRoom()?.send("match:finish", {});
1789
- },
1790
- retry() {
1791
- },
1792
- cancel() {
1793
- session.leave();
1794
- live = false;
1795
- }
1796
- };
1797
- }
1798
- async function createPrivate(opts) {
1799
- const client = await relayClient(opts);
1800
- const room = await client.create(MATCH_ROOM_TYPE, matchJoinOptions(opts));
1801
- const handle = wirePrivate(client, room);
1802
- Object.defineProperty(handle, "code", { value: room.roomId, enumerable: true });
1803
- return handle;
1804
- }
1805
- async function joinPrivate(code, opts) {
1806
- const client = await relayClient(opts);
1807
- const room = await client.joinById(code, matchJoinOptions(opts));
1808
- return wirePrivate(client, room);
1809
- }
1
+ import {
2
+ connect,
3
+ createPrivate,
4
+ joinPrivate,
5
+ matchmake
6
+ } from "./chunk-CTDU4IJ5.js";
7
+ import "./chunk-C52XFIB6.js";
1810
8
  export {
1811
9
  connect,
1812
10
  createPrivate,