twinwire 0.0.0-stage → 1.0.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 (95) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -2
  4. package/dist/browser/twinwire.js +2075 -0
  5. package/dist/browser/twinwire.js.map +7 -0
  6. package/dist/browser/twinwire.min.js +1 -0
  7. package/dist/browser/twinwire.mjs +2052 -0
  8. package/dist/browser/twinwire.mjs.map +7 -0
  9. package/dist/browser.d.ts +5 -0
  10. package/dist/browser.js +6 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/client/client.d.ts +128 -0
  13. package/dist/client/client.js +470 -0
  14. package/dist/client/client.js.map +1 -0
  15. package/dist/client/index.d.ts +3 -0
  16. package/dist/client/index.js +4 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client/lobby.d.ts +40 -0
  19. package/dist/client/lobby.js +122 -0
  20. package/dist/client/lobby.js.map +1 -0
  21. package/dist/client/rtc.d.ts +17 -0
  22. package/dist/client/rtc.js +102 -0
  23. package/dist/client/rtc.js.map +1 -0
  24. package/dist/core/budget.d.ts +27 -0
  25. package/dist/core/budget.js +39 -0
  26. package/dist/core/budget.js.map +1 -0
  27. package/dist/core/endpoint.d.ts +144 -0
  28. package/dist/core/endpoint.js +501 -0
  29. package/dist/core/endpoint.js.map +1 -0
  30. package/dist/core/lobby.d.ts +59 -0
  31. package/dist/core/lobby.js +53 -0
  32. package/dist/core/lobby.js.map +1 -0
  33. package/dist/core/protocol.d.ts +52 -0
  34. package/dist/core/protocol.js +100 -0
  35. package/dist/core/protocol.js.map +1 -0
  36. package/dist/core/reliable.d.ts +52 -0
  37. package/dist/core/reliable.js +116 -0
  38. package/dist/core/reliable.js.map +1 -0
  39. package/dist/core/rtc.d.ts +28 -0
  40. package/dist/core/rtc.js +21 -0
  41. package/dist/core/rtc.js.map +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.js +7 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/netcode/clock.d.ts +15 -0
  46. package/dist/netcode/clock.js +37 -0
  47. package/dist/netcode/clock.js.map +1 -0
  48. package/dist/netcode/codec.d.ts +10 -0
  49. package/dist/netcode/codec.js +95 -0
  50. package/dist/netcode/codec.js.map +1 -0
  51. package/dist/netcode/index.d.ts +9 -0
  52. package/dist/netcode/index.js +10 -0
  53. package/dist/netcode/index.js.map +1 -0
  54. package/dist/netcode/inputs.d.ts +7 -0
  55. package/dist/netcode/inputs.js +43 -0
  56. package/dist/netcode/inputs.js.map +1 -0
  57. package/dist/netcode/interpolation.d.ts +44 -0
  58. package/dist/netcode/interpolation.js +101 -0
  59. package/dist/netcode/interpolation.js.map +1 -0
  60. package/dist/netcode/prediction.d.ts +40 -0
  61. package/dist/netcode/prediction.js +97 -0
  62. package/dist/netcode/prediction.js.map +1 -0
  63. package/dist/netcode/replication.d.ts +36 -0
  64. package/dist/netcode/replication.js +90 -0
  65. package/dist/netcode/replication.js.map +1 -0
  66. package/dist/netcode/snapshots.d.ts +29 -0
  67. package/dist/netcode/snapshots.js +78 -0
  68. package/dist/netcode/snapshots.js.map +1 -0
  69. package/dist/netcode/spatial.d.ts +16 -0
  70. package/dist/netcode/spatial.js +63 -0
  71. package/dist/netcode/spatial.js.map +1 -0
  72. package/dist/netcode/timing.d.ts +37 -0
  73. package/dist/netcode/timing.js +41 -0
  74. package/dist/netcode/timing.js.map +1 -0
  75. package/dist/server/index.d.ts +4 -0
  76. package/dist/server/index.js +5 -0
  77. package/dist/server/index.js.map +1 -0
  78. package/dist/server/lobby.d.ts +134 -0
  79. package/dist/server/lobby.js +462 -0
  80. package/dist/server/lobby.js.map +1 -0
  81. package/dist/server/rtc.d.ts +38 -0
  82. package/dist/server/rtc.js +102 -0
  83. package/dist/server/rtc.js.map +1 -0
  84. package/dist/server/server.d.ts +145 -0
  85. package/dist/server/server.js +457 -0
  86. package/dist/server/server.js.map +1 -0
  87. package/dist/server/turn.d.ts +22 -0
  88. package/dist/server/turn.js +52 -0
  89. package/dist/server/turn.js.map +1 -0
  90. package/docs/ADOPTING.md +64 -0
  91. package/docs/API.md +181 -0
  92. package/docs/ARCHITECTURE.md +101 -0
  93. package/docs/OPERATIONS.md +93 -0
  94. package/docs/RELEASE-CHECKLIST.md +25 -0
  95. package/package.json +100 -4
@@ -0,0 +1,2052 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/core/budget.ts
6
+ var TokenBucket = class {
7
+ constructor(rate, capacity, now) {
8
+ __publicField(this, "rate", rate);
9
+ __publicField(this, "capacity", capacity);
10
+ __publicField(this, "tokens");
11
+ __publicField(this, "at");
12
+ if (!(rate > 0) || !Number.isFinite(rate) || !(capacity > 0) || !Number.isFinite(capacity)) throw new RangeError("Invalid budget");
13
+ this.tokens = capacity;
14
+ this.at = now;
15
+ }
16
+ available(now) {
17
+ this.tokens = Math.min(this.capacity, this.tokens + Math.max(0, now - this.at) * this.rate / 1e3);
18
+ this.at = Math.max(this.at, now);
19
+ return this.tokens;
20
+ }
21
+ /** A message larger than the burst may go once the bucket is full; the debt then delays what follows. */
22
+ take(bytes, now) {
23
+ if (!Number.isFinite(bytes) || bytes < 0) throw new RangeError("Invalid byte count");
24
+ if (this.available(now) < Math.min(bytes, this.capacity)) return false;
25
+ this.tokens -= bytes;
26
+ return true;
27
+ }
28
+ };
29
+
30
+ // src/core/protocol.ts
31
+ var VERSION = 3;
32
+ var HEADER_BYTES = 22;
33
+ var MTU_STATE_BYTES = 1100 - HEADER_BYTES;
34
+ var MAX_FRAME_BYTES = 256 * 1024;
35
+ var MAX_STATE_BYTES = MAX_FRAME_BYTES - HEADER_BYTES;
36
+ var DEFAULT_STATE_BYTES = 64 * 1024;
37
+ var MAX_CONTROL_BYTES = 128 * 1024;
38
+ var MAGIC = 22356;
39
+ var FrameKind = { state: 1, probe: 2, reply: 3, events: 4, sack: 5, stateAcked: 6 };
40
+ var KINDS = new Set(Object.values(FrameKind));
41
+ var isUint = (v, max = 4294967295) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= max;
42
+ var empty = (k) => k === FrameKind.probe || k === FrameKind.reply;
43
+ var streamed = (k) => k === FrameKind.state || k === FrameKind.stateAcked;
44
+ function encodeFrame(f) {
45
+ if (!KINDS.has(f.kind) || !isUint(f.epoch) || !f.epoch || !isUint(f.stream, 65535) || !isUint(f.seq) || !Number.isFinite(f.time) || f.payload.length > MAX_STATE_BYTES || empty(f.kind) && f.payload.length !== 0 || !streamed(f.kind) && f.stream !== 0) throw new RangeError("Invalid frame");
46
+ const b = new Uint8Array(HEADER_BYTES + f.payload.length), v = new DataView(b.buffer);
47
+ v.setUint16(0, MAGIC, true);
48
+ v.setUint8(2, VERSION);
49
+ v.setUint8(3, f.kind);
50
+ v.setUint32(4, f.epoch, true);
51
+ v.setUint16(8, f.stream, true);
52
+ v.setUint32(10, f.seq, true);
53
+ v.setFloat64(14, f.time, true);
54
+ b.set(f.payload, HEADER_BYTES);
55
+ return b;
56
+ }
57
+ function decodeFrame(b, maxBytes = MAX_FRAME_BYTES) {
58
+ if (b.byteLength < HEADER_BYTES || b.byteLength > Math.min(maxBytes, MAX_FRAME_BYTES)) return null;
59
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
60
+ if (v.getUint16(0, true) !== MAGIC || v.getUint8(2) !== VERSION) return null;
61
+ const kind = v.getUint8(3), epoch = v.getUint32(4, true), time = v.getFloat64(14, true), stream = v.getUint16(8, true);
62
+ if (!KINDS.has(kind) || !epoch || !Number.isFinite(time) || empty(kind) && b.byteLength !== HEADER_BYTES || !streamed(kind) && stream !== 0) return null;
63
+ return { kind, epoch, stream, seq: v.getUint32(10, true), time, payload: b.slice(HEADER_BYTES) };
64
+ }
65
+ function newer(a, b) {
66
+ return a !== b && a - b >>> 0 < 2147483648;
67
+ }
68
+ function record(value) {
69
+ return value !== null && typeof value === "object" && !Array.isArray(value);
70
+ }
71
+ var encoder = new TextEncoder();
72
+ var decoder = new TextDecoder();
73
+ var utf8 = (s) => encoder.encode(s);
74
+ var fromUtf8 = (b) => decoder.decode(b);
75
+ function utf8Length(s) {
76
+ let n = 0;
77
+ for (let i = 0; i < s.length; i++) {
78
+ const c = s.charCodeAt(i);
79
+ if (c < 128) n += 1;
80
+ else if (c < 2048) n += 2;
81
+ else if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
82
+ n += 4;
83
+ i++;
84
+ } else n += 3;
85
+ }
86
+ return n;
87
+ }
88
+ function readControl(raw, maxBytes = MAX_CONTROL_BYTES) {
89
+ if (raw.length > maxBytes || utf8Length(raw) > maxBytes) return null;
90
+ try {
91
+ const m = JSON.parse(raw);
92
+ return record(m) && typeof m.t === "string" ? m : null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ function stringify(value, maxBytes = MAX_CONTROL_BYTES) {
98
+ const s = JSON.stringify(value);
99
+ if (s === void 0) throw new TypeError("Value is not JSON");
100
+ if (utf8Length(s) > maxBytes) throw new RangeError("Control frame too large");
101
+ return s;
102
+ }
103
+
104
+ // src/core/reliable.ts
105
+ var ReliableEvents = class {
106
+ constructor(o = {}, maxBytes) {
107
+ __publicField(this, "next", 1);
108
+ __publicField(this, "received", 0);
109
+ __publicField(this, "acknowledged", 0);
110
+ __publicField(this, "highestSent", 0);
111
+ __publicField(this, "bytes", 0);
112
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
113
+ __publicField(this, "early", /* @__PURE__ */ new Map());
114
+ __publicField(this, "maxEvents");
115
+ __publicField(this, "maxBytes");
116
+ __publicField(this, "maxEventBytes");
117
+ __publicField(this, "maxEarly");
118
+ const opts = typeof o === "number" ? { maxEvents: o, maxBytes } : o;
119
+ this.maxEvents = opts.maxEvents ?? 512;
120
+ this.maxBytes = opts.maxBytes ?? 1024 * 1024;
121
+ this.maxEventBytes = opts.maxEventBytes ?? 64 * 1024;
122
+ this.maxEarly = opts.maxEarly ?? 256;
123
+ if (![this.maxEvents, this.maxBytes, this.maxEventBytes, this.maxEarly].every((v) => Number.isInteger(v) && v > 0)) throw new RangeError("Invalid event limits");
124
+ }
125
+ push(data) {
126
+ const text = stringify(data, this.maxEventBytes), bytes = utf8Length(text) + 64;
127
+ if (bytes > this.maxEventBytes + 64) throw new RangeError("Event payload too large");
128
+ if (this.pending.size >= this.maxEvents || this.bytes + bytes > this.maxBytes) throw new Error("Reliable outbox full");
129
+ if (!Number.isSafeInteger(this.next + 1)) throw new Error("Reliable sequence exhausted");
130
+ const seq = this.next++;
131
+ this.pending.set(seq, { seq, data: JSON.parse(text), bytes, text, datagramAt: -Infinity, datagramTries: 0 });
132
+ this.bytes += bytes;
133
+ return seq;
134
+ }
135
+ get size() {
136
+ return this.pending.size;
137
+ }
138
+ get queuedBytes() {
139
+ return this.bytes;
140
+ }
141
+ get sentThrough() {
142
+ return this.highestSent;
143
+ }
144
+ entries() {
145
+ return this.pending.values();
146
+ }
147
+ sent(seq) {
148
+ this.highestSent = Math.max(this.highestSent, seq);
149
+ }
150
+ ack(seq) {
151
+ if (!Number.isSafeInteger(seq) || seq < 0 || seq > this.highestSent) throw new Error("Invalid event acknowledgement");
152
+ if (seq <= this.acknowledged) return false;
153
+ this.acknowledged = seq;
154
+ for (const [id, e] of this.pending) {
155
+ if (id > seq) break;
156
+ this.pending.delete(id);
157
+ this.bytes -= e.bytes;
158
+ }
159
+ return true;
160
+ }
161
+ /** Returns how many events were delivered. Early events wait (bounded); duplicates are dropped. */
162
+ receive(seq, data, deliver) {
163
+ if (!Number.isSafeInteger(seq) || seq < 1) throw new Error("Invalid event sequence");
164
+ if (seq <= this.received) return 0;
165
+ if (seq !== this.received + 1) {
166
+ if (seq - this.received > this.maxEarly) throw new Error("Event sequence gap");
167
+ if (!this.early.has(seq)) this.early.set(seq, data);
168
+ return 0;
169
+ }
170
+ deliver(data, seq);
171
+ this.received = seq;
172
+ let n = 1;
173
+ for (let s = seq + 1; this.early.has(s); s++) {
174
+ deliver(this.early.get(s), s);
175
+ this.early.delete(s);
176
+ this.received = s;
177
+ n++;
178
+ }
179
+ for (const s of this.early.keys()) if (s <= this.received) this.early.delete(s);
180
+ return n;
181
+ }
182
+ checkpoint() {
183
+ return {
184
+ next: this.next,
185
+ received: this.received,
186
+ acknowledged: this.acknowledged,
187
+ pending: Array.from(this.pending.values(), (e) => ({ seq: e.seq, data: structuredClone(e.data) }))
188
+ };
189
+ }
190
+ restore(s) {
191
+ if (this.next !== 1 || this.received || this.pending.size) throw new Error("Restore requires a fresh channel");
192
+ if (![s.next, s.received, s.acknowledged].every((v) => Number.isSafeInteger(v) && v >= 0) || s.next < 1 || s.acknowledged >= s.next || !Array.isArray(s.pending) || s.pending.length > this.maxEvents)
193
+ throw new Error("Invalid checkpoint");
194
+ if (s.pending.length !== s.next - s.acknowledged - 1) throw new Error("Incomplete checkpoint");
195
+ const saved = [];
196
+ let total = 0, expected = s.acknowledged + 1;
197
+ for (const e of s.pending) {
198
+ if (e.seq !== expected++) throw new Error("Invalid checkpoint sequence");
199
+ const text = stringify(e.data, this.maxEventBytes), bytes = utf8Length(text) + 64;
200
+ saved.push({ seq: e.seq, data: JSON.parse(text), bytes, text, datagramAt: -Infinity, datagramTries: 0 });
201
+ total += bytes;
202
+ }
203
+ if (total > this.maxBytes) throw new Error("Checkpoint exceeds outbox limit");
204
+ this.next = s.next;
205
+ this.received = s.received;
206
+ this.acknowledged = s.acknowledged;
207
+ this.highestSent = s.next - 1;
208
+ this.pending = new Map(saved.map((e) => [e.seq, e]));
209
+ this.bytes = total;
210
+ }
211
+ };
212
+
213
+ // src/core/endpoint.ts
214
+ var DATAGRAM_EVENT_BYTES = 960;
215
+ var Endpoint = class {
216
+ constructor(o = {}) {
217
+ __publicField(this, "events");
218
+ __publicField(this, "stats", {
219
+ bytesSent: 0,
220
+ bytesReceived: 0,
221
+ statesSent: 0,
222
+ statesReceived: 0,
223
+ stale: 0,
224
+ expired: 0,
225
+ coalesced: 0,
226
+ congested: 0,
227
+ invalid: 0,
228
+ fastTransitions: 0,
229
+ datagramRttMs: 0,
230
+ eventsViaDatagram: 0,
231
+ datagramEventFrames: 0,
232
+ stateAcksSent: 0,
233
+ stateAcksReceived: 0,
234
+ impairedDrops: 0
235
+ });
236
+ __publicField(this, "onState", () => {
237
+ });
238
+ __publicField(this, "onEvent", () => {
239
+ });
240
+ __publicField(this, "onFastDown", () => {
241
+ });
242
+ __publicField(this, "onFastChange", () => {
243
+ });
244
+ __publicField(this, "onEventAck", () => {
245
+ });
246
+ /** The peer has seen state `seq` on `stream` (only for states sent with `ack: true`). */
247
+ __publicField(this, "onStateAck", () => {
248
+ });
249
+ __publicField(this, "reliable");
250
+ __publicField(this, "datagram");
251
+ __publicField(this, "epoch", 0);
252
+ __publicField(this, "sequence", /* @__PURE__ */ new Map());
253
+ __publicField(this, "received", /* @__PURE__ */ new Map());
254
+ __publicField(this, "queued", /* @__PURE__ */ new Map());
255
+ __publicField(this, "acksOut", /* @__PURE__ */ new Map());
256
+ __publicField(this, "acksIn", /* @__PURE__ */ new Map());
257
+ __publicField(this, "lastAckSentAt", -Infinity);
258
+ __publicField(this, "lastEventSent", 0);
259
+ __publicField(this, "datagramAcked", 0);
260
+ __publicField(this, "ackDue", false);
261
+ __publicField(this, "lastProbe", -Infinity);
262
+ __publicField(this, "proofAt", -Infinity);
263
+ __publicField(this, "attachedAt", 0);
264
+ __publicField(this, "probes", /* @__PURE__ */ new Map());
265
+ __publicField(this, "probeSeq", 0);
266
+ __publicField(this, "fast", false);
267
+ __publicField(this, "now");
268
+ __publicField(this, "budget");
269
+ __publicField(this, "highWater");
270
+ __publicField(this, "maxStreams");
271
+ __publicField(this, "probeInterval");
272
+ __publicField(this, "probeTimeout");
273
+ __publicField(this, "maxStateBytes");
274
+ __publicField(this, "datagramStateBytes");
275
+ __publicField(this, "datagramEvents");
276
+ __publicField(this, "stateAckInterval");
277
+ __publicField(this, "impairment");
278
+ this.now = o.now ?? (() => performance.now());
279
+ const rate = o.bytesPerSecond ?? 32 * 1024;
280
+ this.budget = new TokenBucket(rate, o.burstBytes ?? Math.max(rate / 2, 16 * 1024), this.now());
281
+ this.highWater = o.maxBufferedBytes ?? 64 * 1024;
282
+ this.maxStreams = o.maxStreams ?? 1024;
283
+ this.probeInterval = o.probeIntervalMs ?? 250;
284
+ this.probeTimeout = o.probeTimeoutMs ?? 1500;
285
+ this.maxStateBytes = o.maxStateBytes ?? DEFAULT_STATE_BYTES;
286
+ this.datagramStateBytes = o.datagramStateBytes ?? 8 * 1024;
287
+ this.datagramEvents = o.datagramEvents ?? true;
288
+ this.stateAckInterval = o.stateAckIntervalMs ?? 50;
289
+ this.events = new ReliableEvents(o.events);
290
+ if (!(this.highWater > 0) || !isUint(this.maxStreams, 65536) || this.maxStreams < 1 || !(this.probeInterval >= 10) || !(this.probeTimeout >= this.probeInterval * 2) || !isUint(this.maxStateBytes, MAX_STATE_BYTES) || this.maxStateBytes < 1 || !(this.datagramStateBytes >= MTU_STATE_BYTES) || !(this.stateAckInterval >= 0)) throw new RangeError("Invalid endpoint limits");
291
+ if (o.impair) this.setImpairment(o.impair);
292
+ }
293
+ get active() {
294
+ return !!this.reliable?.open;
295
+ }
296
+ get fastActive() {
297
+ return this.fast && !!this.datagram?.open && this.now() - this.proofAt <= this.probeTimeout;
298
+ }
299
+ get pendingStates() {
300
+ return this.queued.size;
301
+ }
302
+ get connectionEpoch() {
303
+ return this.epoch;
304
+ }
305
+ get maxFrameBytes() {
306
+ return this.maxStateBytes + HEADER_BYTES;
307
+ }
308
+ /** Newest seq the peer reported for an acknowledged stream in this connection epoch. */
309
+ stateAck(stream) {
310
+ return this.acksIn.get(stream);
311
+ }
312
+ setImpairment(i) {
313
+ if (!i) {
314
+ this.impairment = void 0;
315
+ return;
316
+ }
317
+ const { lagMs = 0, jitterMs = 0, loss = 0, duplicate = 0 } = i;
318
+ if (![lagMs, jitterMs].every((v) => Number.isFinite(v) && v >= 0 && v <= 1e4) || ![loss, duplicate].every((v) => Number.isFinite(v) && v >= 0 && v <= 1)) throw new RangeError("Invalid impairment");
319
+ this.impairment = lagMs || jitterMs || loss || duplicate ? { lagMs, jitterMs, loss, duplicate } : void 0;
320
+ }
321
+ get impaired() {
322
+ return this.impairment && { ...this.impairment };
323
+ }
324
+ attach(epoch, reliable) {
325
+ if (!isUint(epoch) || !epoch) throw new Error("Invalid epoch");
326
+ this.detach();
327
+ this.epoch = epoch;
328
+ this.reliable = this.impairReliable(reliable);
329
+ this.ackDue = this.events.received > 0;
330
+ this.lastEventSent = this.events.acknowledged;
331
+ this.datagramAcked = 0;
332
+ this.sequence.clear();
333
+ this.received.clear();
334
+ this.acksOut.clear();
335
+ this.acksIn.clear();
336
+ for (const e of this.events.entries()) {
337
+ e.datagramAt = -Infinity;
338
+ e.datagramTries = 0;
339
+ }
340
+ }
341
+ detach() {
342
+ this.dropDatagram(false);
343
+ this.reliable = void 0;
344
+ this.queued.clear();
345
+ }
346
+ attachDatagram(link) {
347
+ this.dropDatagram(false);
348
+ this.datagram = this.impairDatagram(link);
349
+ this.attachedAt = this.now();
350
+ this.lastProbe = -Infinity;
351
+ this.proofAt = -Infinity;
352
+ this.probes.clear();
353
+ }
354
+ dropDatagram(notify = true) {
355
+ const d = this.datagram;
356
+ this.datagram = void 0;
357
+ this.proofAt = -Infinity;
358
+ this.probes.clear();
359
+ this.setFast(false);
360
+ try {
361
+ d?.close();
362
+ } catch {
363
+ }
364
+ if (d && notify) this.onFastDown();
365
+ }
366
+ setFast(next) {
367
+ if (next !== this.fast) {
368
+ this.fast = next;
369
+ this.stats.fastTransitions++;
370
+ this.onFastChange(next);
371
+ }
372
+ }
373
+ sendControl(m) {
374
+ const data = stringify(m), ws = this.reliable;
375
+ if (!ws?.open || ws.bufferedAmount > this.highWater) return false;
376
+ try {
377
+ if (!ws.send(data)) return false;
378
+ } catch {
379
+ return false;
380
+ }
381
+ this.stats.bytesSent += utf8Length(data);
382
+ return true;
383
+ }
384
+ sendEvent(data) {
385
+ const seq = this.events.push(data);
386
+ this.flush();
387
+ return seq;
388
+ }
389
+ /** Retains only the newest unsent payload per stream. Returns the state's seq (0 when offline).
390
+ * An accepted enqueue is not a delivery guarantee; use `ack: true` and onStateAck to learn what arrived. */
391
+ sendState(stream, data, time, o = {}) {
392
+ if (!this.active) return 0;
393
+ if (!isUint(stream, 65535) || !Number.isFinite(time) || data.length > this.maxStateBytes || !(o.ttlMs === void 0 || Number.isFinite(o.ttlMs) && o.ttlMs > 0) || !(o.priority === void 0 || Number.isFinite(o.priority) && o.priority >= 0)) throw new RangeError("Invalid state");
394
+ if (!this.sequence.has(stream) && this.sequence.size >= this.maxStreams) throw new Error("State stream limit");
395
+ const seq = (this.sequence.get(stream) ?? 0) + 1 >>> 0 || 1;
396
+ this.sequence.set(stream, seq);
397
+ const old = this.queued.get(stream);
398
+ if (old) this.stats.coalesced++;
399
+ const now = this.now();
400
+ this.queued.set(stream, {
401
+ frame: { kind: o.ack ? FrameKind.stateAcked : FrameKind.state, epoch: this.epoch, stream, seq, time, payload: data.slice() },
402
+ first: old?.first ?? now,
403
+ deadline: now + (o.ttlMs ?? 100),
404
+ priority: o.priority ?? 1
405
+ });
406
+ return seq;
407
+ }
408
+ receiveControl(m) {
409
+ if (m.e !== this.epoch || !this.active) return false;
410
+ if (m.t === "event") {
411
+ if (typeof m.s !== "number" || !("d" in m)) throw new Error("Invalid event");
412
+ this.events.receive(m.s, m.d, this.onEvent);
413
+ this.ackDue = !this.sendControl({ t: "ack", e: this.epoch, s: this.events.received });
414
+ return true;
415
+ }
416
+ if (m.t === "ack") {
417
+ if (typeof m.s !== "number") throw new Error("Invalid acknowledgement");
418
+ if (this.events.ack(m.s)) this.onEventAck(m.s);
419
+ return true;
420
+ }
421
+ return false;
422
+ }
423
+ receiveBinary(bytes, via) {
424
+ this.stats.bytesReceived += bytes.length;
425
+ if (!this.active) return;
426
+ const f = decodeFrame(bytes, this.maxFrameBytes);
427
+ if (!f) {
428
+ this.stats.invalid++;
429
+ return;
430
+ }
431
+ if (f.epoch !== this.epoch || via === "rtc" && !this.datagram) {
432
+ this.stats.stale++;
433
+ return;
434
+ }
435
+ switch (f.kind) {
436
+ case FrameKind.probe:
437
+ if (via === "rtc") this.sendFast(encodeFrame({ ...f, kind: FrameKind.reply }));
438
+ return;
439
+ case FrameKind.reply: {
440
+ if (via !== "rtc") return;
441
+ const sent = this.probes.get(f.seq);
442
+ if (sent === void 0 || sent !== f.time) return;
443
+ this.probes.delete(f.seq);
444
+ const rtt = this.now() - sent;
445
+ if (rtt < 0 || rtt > this.probeTimeout) return;
446
+ this.proofAt = this.now();
447
+ this.stats.datagramRttMs = rtt;
448
+ this.setFast(true);
449
+ return;
450
+ }
451
+ case FrameKind.events:
452
+ return this.receiveDatagramEvents(f);
453
+ case FrameKind.sack:
454
+ return this.receiveStateAcks(f);
455
+ }
456
+ const prior = this.received.get(f.stream);
457
+ if (prior !== void 0 && !newer(f.seq, prior)) {
458
+ this.stats.stale++;
459
+ return;
460
+ }
461
+ if (prior === void 0 && this.received.size >= this.maxStreams) {
462
+ this.stats.invalid++;
463
+ return;
464
+ }
465
+ this.received.set(f.stream, f.seq);
466
+ this.stats.statesReceived++;
467
+ if (f.kind === FrameKind.stateAcked) this.acksOut.set(f.stream, f.seq);
468
+ this.onState({ stream: f.stream, seq: f.seq, time: f.time, data: f.payload, via });
469
+ }
470
+ receiveDatagramEvents(f) {
471
+ if (f.seq <= this.events.sentThrough && this.events.ack(f.seq)) this.onEventAck(f.seq);
472
+ if (!f.payload.length) return;
473
+ let list;
474
+ try {
475
+ list = JSON.parse(fromUtf8(f.payload));
476
+ } catch {
477
+ this.stats.invalid++;
478
+ return;
479
+ }
480
+ if (!Array.isArray(list) || list.length > 64) {
481
+ this.stats.invalid++;
482
+ return;
483
+ }
484
+ let delivered = 0;
485
+ for (const item of list) {
486
+ if (!Array.isArray(item) || item.length !== 2 || !Number.isSafeInteger(item[0])) {
487
+ this.stats.invalid++;
488
+ return;
489
+ }
490
+ try {
491
+ delivered += this.events.receive(item[0], item[1], this.onEvent);
492
+ } catch {
493
+ this.stats.invalid++;
494
+ return;
495
+ }
496
+ }
497
+ if (delivered) {
498
+ this.stats.eventsViaDatagram += delivered;
499
+ this.ackDue = true;
500
+ }
501
+ }
502
+ receiveStateAcks(f) {
503
+ const p = f.payload;
504
+ if (p.length % 6) {
505
+ this.stats.invalid++;
506
+ return;
507
+ }
508
+ const v = new DataView(p.buffer, p.byteOffset, p.byteLength);
509
+ for (let at = 0; at < p.length; at += 6) {
510
+ const stream = v.getUint16(at, true), seq = v.getUint32(at + 2, true), sent = this.sequence.get(stream);
511
+ if (sent === void 0 || newer(seq, sent)) continue;
512
+ const prior = this.acksIn.get(stream);
513
+ if (prior !== void 0 && !newer(seq, prior)) continue;
514
+ this.acksIn.set(stream, seq);
515
+ this.stats.stateAcksReceived++;
516
+ this.onStateAck(stream, seq);
517
+ }
518
+ }
519
+ sendFast(b) {
520
+ const d = this.datagram;
521
+ if (!d?.open || d.bufferedAmount > this.highWater) return false;
522
+ try {
523
+ if (!d.send(b)) return false;
524
+ } catch {
525
+ return false;
526
+ }
527
+ this.stats.bytesSent += b.length;
528
+ return true;
529
+ }
530
+ /** Binary on the datagram path when it is proven, else the WebSocket. */
531
+ sendBinary(b, preferFast) {
532
+ const ws = this.reliable;
533
+ if (preferFast && this.fastActive && this.datagram && this.datagram.bufferedAmount <= this.highWater) return this.sendFast(b);
534
+ if (!ws?.open || ws.bufferedAmount > this.highWater) return false;
535
+ try {
536
+ if (!ws.send(b)) return false;
537
+ } catch {
538
+ return false;
539
+ }
540
+ this.stats.bytesSent += b.length;
541
+ return true;
542
+ }
543
+ pump() {
544
+ const now = this.now(), d = this.datagram;
545
+ if (d) {
546
+ if (!d.open && now - this.attachedAt > this.probeTimeout || this.fast && now - this.proofAt > this.probeTimeout || !this.fast && now - this.attachedAt > this.probeTimeout * 2) this.dropDatagram();
547
+ else if (d.open && now - this.lastProbe >= this.probeInterval) {
548
+ this.lastProbe = now;
549
+ const seq = this.probeSeq = this.probeSeq + 1 >>> 0;
550
+ this.probes.set(seq, now);
551
+ for (const [s, at] of this.probes) if (now - at > this.probeTimeout) this.probes.delete(s);
552
+ this.sendFast(encodeFrame({ kind: FrameKind.probe, epoch: this.epoch, stream: 0, seq, time: now, payload: new Uint8Array() }));
553
+ }
554
+ }
555
+ this.flush();
556
+ }
557
+ flush(maxPackets = 8) {
558
+ const ws = this.reliable, now = this.now();
559
+ if (!ws?.open) return;
560
+ if (this.ackDue) this.ackDue = !this.sendControl({ t: "ack", e: this.epoch, s: this.events.received });
561
+ for (const e of this.events.entries()) {
562
+ if (e.seq <= this.lastEventSent) continue;
563
+ const encoded = `{"t":"event","e":${this.epoch},"s":${e.seq},"d":${e.text}}`;
564
+ const size = utf8Length(encoded);
565
+ if (ws.bufferedAmount > this.highWater || !this.budget.take(size, now)) break;
566
+ try {
567
+ if (!ws.send(encoded)) break;
568
+ } catch {
569
+ break;
570
+ }
571
+ this.events.sent(e.seq);
572
+ this.lastEventSent = e.seq;
573
+ this.stats.bytesSent += size;
574
+ }
575
+ if (this.fastActive) this.flushDatagramEvents(now);
576
+ if (this.acksOut.size && now - this.lastAckSentAt >= this.stateAckInterval) this.flushStateAcks(now);
577
+ for (const [id, q] of this.queued) if (q.deadline <= now) {
578
+ this.queued.delete(id);
579
+ this.stats.expired++;
580
+ }
581
+ if (!this.queued.size) return;
582
+ const queue = [...this.queued.values()].sort((a, b) => b.priority + (now - b.first) / 100 - (a.priority + (now - a.first) / 100));
583
+ let count = 0;
584
+ for (const q of queue) {
585
+ if (count++ >= maxPackets) break;
586
+ const b = encodeFrame(q.frame);
587
+ if (!this.budget.take(b.length, now)) break;
588
+ if (!this.sendBinary(b, q.frame.payload.length <= this.datagramStateBytes)) {
589
+ this.stats.congested++;
590
+ break;
591
+ }
592
+ this.stats.statesSent++;
593
+ this.queued.delete(q.frame.stream);
594
+ }
595
+ }
596
+ /** Copies of WebSocket-sent, unacknowledged events, retried on the datagram path for UDP-like latency. */
597
+ flushDatagramEvents(now) {
598
+ const retry = Math.max(40, Math.min(200, this.stats.datagramRttMs * 1.5 + 10));
599
+ const items = [];
600
+ let bytes = 2;
601
+ if (this.datagramEvents) for (const e of this.events.entries()) {
602
+ if (e.seq > this.lastEventSent) break;
603
+ if (e.datagramTries >= 6 || now - e.datagramAt < retry || e.bytes - 64 > DATAGRAM_EVENT_BYTES) continue;
604
+ const item = `[${e.seq},${e.text}]`, size = utf8Length(item) + 1;
605
+ if (bytes + size > DATAGRAM_EVENT_BYTES || items.length >= 64) break;
606
+ items.push(item);
607
+ bytes += size;
608
+ e.datagramAt = now;
609
+ e.datagramTries++;
610
+ }
611
+ const ackChanged = this.events.received !== this.datagramAcked;
612
+ if (!items.length && !ackChanged) return;
613
+ const payload = utf8("[" + items.join(",") + "]");
614
+ if (this.sendFast(encodeFrame({ kind: FrameKind.events, epoch: this.epoch, stream: 0, seq: this.events.received >>> 0, time: now, payload }))) {
615
+ this.datagramAcked = this.events.received;
616
+ this.stats.datagramEventFrames++;
617
+ }
618
+ }
619
+ flushStateAcks(now) {
620
+ const pairs = [...this.acksOut].slice(0, 150), p = new Uint8Array(pairs.length * 6), v = new DataView(p.buffer);
621
+ pairs.forEach(([stream, seq], i) => {
622
+ v.setUint16(i * 6, stream, true);
623
+ v.setUint32(i * 6 + 2, seq, true);
624
+ });
625
+ if (this.sendBinary(encodeFrame({ kind: FrameKind.sack, epoch: this.epoch, stream: 0, seq: 0, time: now, payload: p }), true)) {
626
+ for (const [stream, seq] of pairs) if (this.acksOut.get(stream) === seq) this.acksOut.delete(stream);
627
+ this.lastAckSentAt = now;
628
+ this.stats.stateAcksSent++;
629
+ }
630
+ }
631
+ /** Delays a stream-like link without ever reordering it: one queue, one timer. */
632
+ impairReliable(link) {
633
+ let tail = 0, timer;
634
+ const queue = [];
635
+ const drain = () => {
636
+ timer = void 0;
637
+ const t = performance.now();
638
+ while (queue.length && queue[0].at <= t + 1) {
639
+ const { data } = queue.shift();
640
+ if (link.open) try {
641
+ link.send(data);
642
+ } catch {
643
+ }
644
+ }
645
+ if (queue.length) timer = setTimeout(drain, queue[0].at - t);
646
+ };
647
+ const self = this;
648
+ return {
649
+ get open() {
650
+ return link.open;
651
+ },
652
+ get bufferedAmount() {
653
+ return link.bufferedAmount;
654
+ },
655
+ close: () => {
656
+ queue.length = 0;
657
+ clearTimeout(timer);
658
+ timer = void 0;
659
+ link.close();
660
+ },
661
+ send(data) {
662
+ const i = self.impairment;
663
+ if (!i && !queue.length) return link.send(data);
664
+ if (!link.open) return false;
665
+ const t = performance.now(), at = Math.max(tail, t + (i?.lagMs ?? 0) + Math.random() * (i?.jitterMs ?? 0));
666
+ tail = at;
667
+ queue.push({ at, data: typeof data === "string" ? data : data.slice() });
668
+ if (!timer) timer = setTimeout(drain, at - t);
669
+ return true;
670
+ }
671
+ };
672
+ }
673
+ impairDatagram(link) {
674
+ const self = this;
675
+ return {
676
+ get open() {
677
+ return link.open;
678
+ },
679
+ get bufferedAmount() {
680
+ return link.bufferedAmount;
681
+ },
682
+ close: () => link.close(),
683
+ send(data) {
684
+ const i = self.impairment;
685
+ if (!i) return link.send(data);
686
+ if (!link.open) return false;
687
+ const copies = Math.random() < i.loss ? 0 : Math.random() < i.duplicate ? 2 : 1;
688
+ if (!copies) self.stats.impairedDrops++;
689
+ for (let n = 0; n < copies; n++) {
690
+ const copy = data.slice();
691
+ setTimeout(() => {
692
+ if (link.open) try {
693
+ link.send(copy);
694
+ } catch {
695
+ }
696
+ }, i.lagMs + Math.random() * i.jitterMs);
697
+ }
698
+ return true;
699
+ }
700
+ };
701
+ }
702
+ };
703
+
704
+ // src/core/rtc.ts
705
+ function validSignal(m) {
706
+ if (!m || typeof m !== "object") return false;
707
+ const v = m;
708
+ if (v.kind === "offer" || v.kind === "answer") return typeof v.sdp === "string" && v.sdp.length > 0 && v.sdp.length <= 12e3;
709
+ return v.kind === "candidate" && typeof v.candidate === "string" && v.candidate.length <= 1500 && typeof v.mid === "string" && v.mid.length <= 128;
710
+ }
711
+ var ICE_URL = /^(stuns?|turns?):[^\s]{1,250}$/;
712
+ function validIceServers(v) {
713
+ return Array.isArray(v) && v.length <= 8 && v.every((s) => {
714
+ if (!s || typeof s !== "object") return false;
715
+ const o = s, urls = Array.isArray(o.urls) ? o.urls : [o.urls];
716
+ return urls.length >= 1 && urls.length <= 8 && urls.every((u) => typeof u === "string" && ICE_URL.test(u)) && (o.username === void 0 || typeof o.username === "string" && o.username.length <= 256) && (o.credential === void 0 || typeof o.credential === "string" && o.credential.length <= 256);
717
+ });
718
+ }
719
+
720
+ // src/netcode/clock.ts
721
+ var NetworkClock = class {
722
+ constructor() {
723
+ __publicField(this, "samples", []);
724
+ __publicField(this, "offset", 0);
725
+ __publicField(this, "target", 0);
726
+ __publicField(this, "initialized", false);
727
+ __publicField(this, "last", -Infinity);
728
+ __publicField(this, "rttMs", 0);
729
+ __publicField(this, "uncertaintyMs", Infinity);
730
+ }
731
+ get synced() {
732
+ return this.initialized;
733
+ }
734
+ reset() {
735
+ this.samples = [];
736
+ this.offset = this.target = 0;
737
+ this.initialized = false;
738
+ this.last = -Infinity;
739
+ this.uncertaintyMs = Infinity;
740
+ }
741
+ observe(sent, received, remoteTime) {
742
+ const rtt = received - sent;
743
+ if (![sent, received, remoteTime].every(Number.isFinite) || rtt < 0 || rtt > 15e3) return false;
744
+ this.samples.push({ rtt, offset: remoteTime - (sent + received) / 2 });
745
+ if (this.samples.length > 16) this.samples.shift();
746
+ const best = [...this.samples].sort((a, b) => a.rtt - b.rtt).slice(0, 3);
747
+ this.target = best.reduce((sum, s) => sum + s.offset, 0) / best.length;
748
+ this.rttMs = best[0].rtt;
749
+ this.uncertaintyMs = Math.max(...best.map((s) => Math.abs(s.offset - this.target) + s.rtt / 2));
750
+ if (!this.initialized) {
751
+ this.offset = this.target;
752
+ this.initialized = true;
753
+ }
754
+ return true;
755
+ }
756
+ advance(dtMs) {
757
+ if (!Number.isFinite(dtMs) || dtMs < 0) throw new RangeError("Invalid time step");
758
+ const max = dtMs * 0.05;
759
+ this.offset += Math.max(-max, Math.min(max, this.target - this.offset));
760
+ }
761
+ serverTime(localTime) {
762
+ this.last = Math.max(this.last, localTime + this.offset);
763
+ return this.last;
764
+ }
765
+ };
766
+
767
+ // src/client/rtc.ts
768
+ var BrowserRtcPeer = class {
769
+ constructor(cb, config = { iceServers: [] }) {
770
+ __publicField(this, "cb", cb);
771
+ __publicField(this, "pc");
772
+ __publicField(this, "dc");
773
+ __publicField(this, "alive", true);
774
+ __publicField(this, "offered", false);
775
+ __publicField(this, "remoteReady", false);
776
+ __publicField(this, "localCandidates", []);
777
+ __publicField(this, "remoteCandidates", []);
778
+ __publicField(this, "chain", Promise.resolve());
779
+ this.pc = new RTCPeerConnection(config);
780
+ this.dc = this.pc.createDataChannel("twinwire", { negotiated: true, id: 0, ordered: false, maxRetransmits: 0 });
781
+ const pc = this.pc, dc = this.dc;
782
+ dc.binaryType = "arraybuffer";
783
+ dc.onopen = () => {
784
+ if (this.alive) cb.open({
785
+ get open() {
786
+ return dc.readyState === "open";
787
+ },
788
+ get bufferedAmount() {
789
+ return dc.bufferedAmount;
790
+ },
791
+ send(bytes) {
792
+ if (dc.readyState !== "open") return false;
793
+ dc.send(bytes.slice().buffer);
794
+ return true;
795
+ },
796
+ close: () => this.close()
797
+ });
798
+ };
799
+ dc.onmessage = (e) => {
800
+ if (this.alive && e.data instanceof ArrayBuffer) cb.data(new Uint8Array(e.data));
801
+ };
802
+ dc.onclose = () => {
803
+ if (this.alive) {
804
+ this.close();
805
+ cb.closed();
806
+ }
807
+ };
808
+ dc.onerror = () => {
809
+ if (this.alive) cb.error(new Error("RTC data channel error"));
810
+ };
811
+ pc.onconnectionstatechange = () => {
812
+ if (this.alive && pc.connectionState === "failed") {
813
+ this.close();
814
+ cb.closed();
815
+ }
816
+ };
817
+ pc.onicecandidate = (e) => {
818
+ if (!this.alive || !e.candidate?.candidate) return;
819
+ const m = { kind: "candidate", candidate: e.candidate.candidate, mid: e.candidate.sdpMid ?? "0" };
820
+ if (this.offered) cb.signal(m);
821
+ else if (this.localCandidates.length < 64) this.localCandidates.push(m);
822
+ };
823
+ }
824
+ async offer() {
825
+ await this.pc.setLocalDescription(await this.pc.createOffer());
826
+ if (!this.alive) return;
827
+ this.cb.signal({ kind: "offer", sdp: this.pc.localDescription.sdp });
828
+ this.offered = true;
829
+ for (const c of this.localCandidates) this.cb.signal(c);
830
+ this.localCandidates = [];
831
+ }
832
+ signal(message) {
833
+ this.chain = this.chain.then(async () => {
834
+ if (!this.alive || !validSignal(message)) return;
835
+ if (message.kind === "answer") {
836
+ await this.pc.setRemoteDescription({ type: "answer", sdp: message.sdp });
837
+ if (!this.alive) return;
838
+ this.remoteReady = true;
839
+ for (const c of this.remoteCandidates) await this.pc.addIceCandidate(c);
840
+ this.remoteCandidates = [];
841
+ } else if (message.kind === "candidate") {
842
+ const c = { candidate: message.candidate, sdpMid: message.mid };
843
+ if (this.remoteReady) await this.pc.addIceCandidate(c);
844
+ else {
845
+ if (this.remoteCandidates.length >= 64) throw new Error("Too many ICE candidates");
846
+ this.remoteCandidates.push(c);
847
+ }
848
+ }
849
+ }).catch((e) => {
850
+ if (this.alive) this.cb.error(e instanceof Error ? e : new Error(String(e)));
851
+ });
852
+ return this.chain;
853
+ }
854
+ close() {
855
+ if (!this.alive) return;
856
+ this.alive = false;
857
+ this.localCandidates = [];
858
+ this.remoteCandidates = [];
859
+ this.dc.onopen = this.dc.onmessage = this.dc.onclose = this.dc.onerror = null;
860
+ this.pc.onicecandidate = this.pc.onconnectionstatechange = null;
861
+ try {
862
+ this.dc.close();
863
+ this.pc.close();
864
+ } catch {
865
+ }
866
+ }
867
+ };
868
+
869
+ // src/client/client.ts
870
+ var GameClient = class {
871
+ constructor(options) {
872
+ __publicField(this, "options", options);
873
+ __publicField(this, "endpoint");
874
+ __publicField(this, "clock", new NetworkClock());
875
+ __publicField(this, "status", "idle");
876
+ __publicField(this, "id", null);
877
+ /** ICE servers suggested by the room server (STUN/TURN), used unless options.rtc names its own. */
878
+ __publicField(this, "iceServers", []);
879
+ __publicField(this, "onStatus", () => {
880
+ });
881
+ __publicField(this, "onSession", () => {
882
+ });
883
+ __publicField(this, "onSessionLost", () => {
884
+ });
885
+ __publicField(this, "onState", () => {
886
+ });
887
+ __publicField(this, "onEvent", () => {
888
+ });
889
+ __publicField(this, "onStateAck", () => {
890
+ });
891
+ __publicField(this, "onTransport", () => {
892
+ });
893
+ __publicField(this, "onError", () => {
894
+ });
895
+ __publicField(this, "token", null);
896
+ __publicField(this, "socket");
897
+ __publicField(this, "generation", 0);
898
+ __publicField(this, "epoch", 0);
899
+ __publicField(this, "peer");
900
+ __publicField(this, "rtcAttempt", 0);
901
+ __publicField(this, "rtcStartedAt", 0);
902
+ __publicField(this, "rtcRetryAt", Infinity);
903
+ __publicField(this, "rtcFailures", 0);
904
+ __publicField(this, "reconnects", 0);
905
+ __publicField(this, "retryTimer");
906
+ __publicField(this, "pumpTimer");
907
+ __publicField(this, "lastPump", 0);
908
+ __publicField(this, "lastPing", -Infinity);
909
+ __publicField(this, "lastMessage", 0);
910
+ __publicField(this, "openedAt", 0);
911
+ __publicField(this, "wakeDeadline", Infinity);
912
+ __publicField(this, "pendingPings", /* @__PURE__ */ new Map());
913
+ __publicField(this, "pingId", 0);
914
+ __publicField(this, "stopped", true);
915
+ __publicField(this, "serverRtc", false);
916
+ __publicField(this, "lastTransport", "none");
917
+ __publicField(this, "waiters", []);
918
+ __publicField(this, "lifecycle");
919
+ __publicField(this, "interceptors", []);
920
+ __publicField(this, "listeners", { session: [], lost: [] });
921
+ const u = new URL(options.url);
922
+ if (!["ws:", "wss:"].includes(u.protocol) || !/^[\w:.-]{1,64}$/.test(options.room)) throw new Error("Invalid URL or room");
923
+ this.endpoint = this.makeEndpoint();
924
+ const cp = options.checkpoint;
925
+ if (cp) {
926
+ if (cp.url !== options.url || cp.room !== options.room || !/^[\w-]{20,128}$/.test(cp.token) || typeof cp.id !== "string")
927
+ throw new Error("Checkpoint belongs to a different endpoint or is invalid");
928
+ this.endpoint.events.restore(cp.events);
929
+ this.token = cp.token;
930
+ this.id = cp.id;
931
+ }
932
+ }
933
+ makeEndpoint() {
934
+ const e = new Endpoint({ ...this.options.endpoint, impair: this.options.impair ?? this.options.endpoint?.impair });
935
+ e.onState = (s) => this.onState(s);
936
+ e.onEvent = (d, s) => {
937
+ for (const f of this.interceptors) if (f(d, s)) return;
938
+ this.onEvent(d, s);
939
+ };
940
+ e.onStateAck = (stream, seq) => this.onStateAck(stream, seq);
941
+ e.onFastDown = () => this.stopRtc(true);
942
+ e.onFastChange = () => this.noteTransport();
943
+ return e;
944
+ }
945
+ /** Consume some events before onEvent sees them (return true to consume). Used by LobbyClient. Returns removal. */
946
+ intercept(fn) {
947
+ this.interceptors.push(fn);
948
+ return () => {
949
+ const i = this.interceptors.indexOf(fn);
950
+ if (i >= 0) this.interceptors.splice(i, 1);
951
+ };
952
+ }
953
+ /** Extra listeners next to onSession/onSessionLost. Returns removal. */
954
+ on(kind, fn) {
955
+ const list = this.listeners[kind];
956
+ list.push(fn);
957
+ return () => {
958
+ const i = list.indexOf(fn);
959
+ if (i >= 0) list.splice(i, 1);
960
+ };
961
+ }
962
+ lost(info) {
963
+ for (const f of this.listeners.lost) try {
964
+ f(info);
965
+ } catch (e) {
966
+ this.report(e);
967
+ }
968
+ this.onSessionLost(info);
969
+ }
970
+ /** Which path state messages take right now. */
971
+ get transport() {
972
+ return this.status !== "online" ? "none" : this.endpoint.fastActive ? "rtc" : "ws";
973
+ }
974
+ noteTransport() {
975
+ const t = this.transport;
976
+ if (t !== this.lastTransport) {
977
+ this.lastTransport = t;
978
+ try {
979
+ this.onTransport(t);
980
+ } catch (e) {
981
+ this.report(e);
982
+ }
983
+ }
984
+ }
985
+ report(error) {
986
+ this.onError(error instanceof Error ? error : new Error(String(error)));
987
+ }
988
+ setStatus(s) {
989
+ if (s === this.status) return;
990
+ this.status = s;
991
+ this.onStatus(s);
992
+ this.noteTransport();
993
+ if (s === "online") for (const w of this.waiters.splice(0)) w.resolve();
994
+ else if (s === "lost" || s === "closed") for (const w of this.waiters.splice(0)) w.reject(new Error(`Session ${s}`));
995
+ }
996
+ /** Resolves when the session is online (at once if it already is); rejects if it is lost or closed first. */
997
+ whenOnline() {
998
+ if (this.status === "online") return Promise.resolve();
999
+ if (this.status === "lost" || this.status === "closed") return Promise.reject(new Error(`Session ${this.status}`));
1000
+ return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
1001
+ }
1002
+ connect() {
1003
+ if (this.status === "online" || this.status === "connecting") return;
1004
+ if (this.status === "lost") throw new Error("Call newSession() after session expiry");
1005
+ this.stopped = false;
1006
+ this.setStatus("connecting");
1007
+ clearInterval(this.pumpTimer);
1008
+ this.lastPump = performance.now();
1009
+ this.pumpTimer = setInterval(() => this.pump(), 1e3 / (this.options.pumpHz ?? 60));
1010
+ this.open();
1011
+ }
1012
+ open() {
1013
+ if (this.stopped) return;
1014
+ clearTimeout(this.retryTimer);
1015
+ this.retryTimer = void 0;
1016
+ const generation = ++this.generation;
1017
+ const now = performance.now();
1018
+ this.openedAt = this.lastMessage = now;
1019
+ let ws;
1020
+ try {
1021
+ ws = (this.options.websocket ?? ((u) => new WebSocket(u)))(this.options.url);
1022
+ } catch (error) {
1023
+ this.report(error);
1024
+ this.retry();
1025
+ return;
1026
+ }
1027
+ this.socket = ws;
1028
+ ws.binaryType = "arraybuffer";
1029
+ ws.onopen = () => {
1030
+ if (generation !== this.generation || this.stopped) {
1031
+ ws.close();
1032
+ return;
1033
+ }
1034
+ ws.send(stringify({
1035
+ t: "hello",
1036
+ v: VERSION,
1037
+ room: this.options.room,
1038
+ token: this.token,
1039
+ auth: this.options.auth ?? null,
1040
+ impair: this.options.impair ?? null
1041
+ }));
1042
+ };
1043
+ ws.onmessage = (ev) => {
1044
+ if (generation !== this.generation || this.stopped) return;
1045
+ this.lastMessage = performance.now();
1046
+ this.wakeDeadline = Infinity;
1047
+ try {
1048
+ if (ev.data instanceof ArrayBuffer) this.endpoint.receiveBinary(new Uint8Array(ev.data), "ws");
1049
+ else if (typeof ev.data === "string") this.message(ev.data, ws);
1050
+ } catch (error) {
1051
+ this.report(error);
1052
+ ws.close(4004, "protocol");
1053
+ }
1054
+ };
1055
+ ws.onerror = () => {
1056
+ };
1057
+ ws.onclose = (ev) => {
1058
+ if (generation !== this.generation) return;
1059
+ this.socket = void 0;
1060
+ this.endpoint.detach();
1061
+ this.stopRtc(false);
1062
+ this.clock.reset();
1063
+ this.pendingPings.clear();
1064
+ if (!this.stopped) {
1065
+ if ([1002, 1008, 4001, 4002, 4004].includes(ev.code)) {
1066
+ this.stopped = true;
1067
+ clearInterval(this.pumpTimer);
1068
+ clearTimeout(this.retryTimer);
1069
+ this.setStatus("lost");
1070
+ this.lost({ pendingEvents: this.endpoint.events.size, reason: ev.reason || `connection rejected (${ev.code})` });
1071
+ } else {
1072
+ this.setStatus("connecting");
1073
+ this.retry();
1074
+ }
1075
+ }
1076
+ };
1077
+ }
1078
+ message(raw, ws) {
1079
+ this.endpoint.stats.bytesReceived += utf8Length(raw);
1080
+ const m = readControl(raw);
1081
+ if (!m) throw new Error("Invalid control message");
1082
+ if (m.t === "reset") {
1083
+ this.stopped = true;
1084
+ clearInterval(this.pumpTimer);
1085
+ clearTimeout(this.retryTimer);
1086
+ this.stopRtc(false);
1087
+ this.endpoint.detach();
1088
+ ws.close();
1089
+ this.setStatus("lost");
1090
+ this.lost({ pendingEvents: this.endpoint.events.size, reason: typeof m.reason === "string" ? m.reason : "expired" });
1091
+ return;
1092
+ }
1093
+ if (m.t === "welcome") {
1094
+ if (this.status !== "connecting" || !isUint(m.e) || !m.e || typeof m.id !== "string" || typeof m.token !== "string" || !/^[\w-]{20,128}$/.test(m.token) || typeof m.resumed !== "boolean" || m.v !== VERSION)
1095
+ throw new Error("Invalid welcome");
1096
+ this.token = m.token;
1097
+ this.id = m.id;
1098
+ this.epoch = m.e;
1099
+ this.reconnects = 0;
1100
+ this.serverRtc = m.rtc === true;
1101
+ this.iceServers = validIceServers(m.ice) ? m.ice : [];
1102
+ this.endpoint.attach(this.epoch, {
1103
+ get open() {
1104
+ return ws.readyState === 1;
1105
+ },
1106
+ get bufferedAmount() {
1107
+ return ws.bufferedAmount;
1108
+ },
1109
+ send(data) {
1110
+ if (ws.readyState !== 1) return false;
1111
+ ws.send(typeof data === "string" ? data : data.slice().buffer);
1112
+ return true;
1113
+ },
1114
+ close() {
1115
+ ws.close();
1116
+ }
1117
+ });
1118
+ this.lastPing = -Infinity;
1119
+ this.clock.reset();
1120
+ this.setStatus("online");
1121
+ const info = { id: this.id, resumed: m.resumed };
1122
+ for (const f of this.listeners.session) try {
1123
+ f(info);
1124
+ } catch (e) {
1125
+ this.report(e);
1126
+ }
1127
+ this.onSession(info);
1128
+ this.rtcFailures = 0;
1129
+ this.rtcRetryAt = performance.now();
1130
+ this.pump();
1131
+ return;
1132
+ }
1133
+ if (m.e !== this.epoch || this.status !== "online") return;
1134
+ if (this.endpoint.receiveControl(m)) return;
1135
+ if (m.t === "pong" && isUint(m.n) && typeof m.s === "number" && Number.isFinite(m.s)) {
1136
+ const at = this.pendingPings.get(m.n);
1137
+ if (at === void 0) return;
1138
+ this.pendingPings.delete(m.n);
1139
+ this.clock.observe(at, performance.now(), m.s);
1140
+ return;
1141
+ }
1142
+ if (m.t === "signal" && m.a === this.rtcAttempt && validSignal(m.d)) {
1143
+ void this.peer?.signal(m.d);
1144
+ return;
1145
+ }
1146
+ if (m.t === "rtc-reset" && m.a === this.rtcAttempt) this.stopRtc(false);
1147
+ }
1148
+ retry() {
1149
+ if (this.stopped) return;
1150
+ clearTimeout(this.retryTimer);
1151
+ const cap = Math.min(this.options.reconnectMaxMs ?? 8e3, (this.options.reconnectBaseMs ?? 200) * 2 ** Math.min(this.reconnects++, 8));
1152
+ this.retryTimer = setTimeout(() => this.open(), cap * (0.5 + Math.random() / 2));
1153
+ }
1154
+ /** Testing: drop the connection as a network failure would (the session resumes by itself). */
1155
+ simulateDrop() {
1156
+ this.socket?.close(4e3, "simulated drop");
1157
+ }
1158
+ /** Skip the backoff wait (the network just came back, the page woke up). */
1159
+ reconnectNow() {
1160
+ if (this.stopped || this.status === "online") return;
1161
+ this.reconnects = 0;
1162
+ if (this.retryTimer !== void 0 || !this.socket) this.open();
1163
+ }
1164
+ rtcConfig() {
1165
+ const own = this.options.rtc || void 0;
1166
+ return own?.iceServers ? own : { ...own, iceServers: this.iceServers };
1167
+ }
1168
+ startRtc() {
1169
+ if (!this.serverRtc || this.peer || this.options.rtc === false || typeof RTCPeerConnection === "undefined" || this.status !== "online") return;
1170
+ const a = ++this.rtcAttempt, e = this.epoch;
1171
+ this.rtcStartedAt = performance.now();
1172
+ this.rtcRetryAt = Infinity;
1173
+ try {
1174
+ const peer = new BrowserRtcPeer({
1175
+ signal: (d) => {
1176
+ if (a === this.rtcAttempt && e === this.epoch) this.endpoint.sendControl({ t: "signal", e, a, d });
1177
+ },
1178
+ open: (link) => {
1179
+ if (a === this.rtcAttempt && e === this.epoch && !this.stopped) this.endpoint.attachDatagram(link);
1180
+ else link.close();
1181
+ },
1182
+ data: (data) => {
1183
+ if (a === this.rtcAttempt && e === this.epoch) this.endpoint.receiveBinary(data, "rtc");
1184
+ },
1185
+ closed: () => {
1186
+ if (a === this.rtcAttempt && e === this.epoch) this.stopRtc(true);
1187
+ },
1188
+ error: (error) => {
1189
+ if (a === this.rtcAttempt && e === this.epoch) {
1190
+ this.report(error);
1191
+ this.stopRtc(true);
1192
+ }
1193
+ }
1194
+ }, this.rtcConfig());
1195
+ this.peer = peer;
1196
+ void peer.offer().catch((error) => {
1197
+ if (this.peer === peer) {
1198
+ this.report(error);
1199
+ this.stopRtc(true);
1200
+ }
1201
+ });
1202
+ } catch (error) {
1203
+ this.report(error);
1204
+ this.stopRtc(true);
1205
+ }
1206
+ }
1207
+ stopRtc(notify) {
1208
+ const a = this.rtcAttempt, peer = this.peer;
1209
+ this.peer = void 0;
1210
+ if (notify && peer) this.endpoint.sendControl({ t: "rtc-reset", e: this.epoch, a });
1211
+ this.endpoint.dropDatagram(false);
1212
+ peer?.close();
1213
+ this.rtcRetryAt = performance.now() + Math.min(6e4, (this.options.rtcRetryMs ?? 3e3) * 2 ** Math.min(this.rtcFailures++, 4));
1214
+ this.noteTransport();
1215
+ }
1216
+ pump() {
1217
+ if (this.stopped) return;
1218
+ const now = performance.now();
1219
+ this.clock.advance(Math.max(0, now - this.lastPump));
1220
+ this.lastPump = now;
1221
+ if (this.status === "online") {
1222
+ if (now - this.lastMessage > (this.options.heartbeatTimeoutMs ?? 8e3) || now > this.wakeDeadline) {
1223
+ this.wakeDeadline = Infinity;
1224
+ this.socket?.close(4e3, "heartbeat timeout");
1225
+ return;
1226
+ }
1227
+ if (now - this.lastPing >= (this.pendingPings.size < 4 && this.clock.uncertaintyMs === Infinity ? 200 : 1e3)) this.ping(now);
1228
+ if (this.peer && now - this.rtcStartedAt > (this.options.rtcConnectTimeoutMs ?? 8e3) && !this.endpoint.fastActive) this.stopRtc(true);
1229
+ if (!this.peer && now >= this.rtcRetryAt) this.startRtc();
1230
+ this.endpoint.pump();
1231
+ this.noteTransport();
1232
+ } else if (this.socket && now - this.openedAt > 1e4) {
1233
+ this.socket.close(4e3, "handshake timeout");
1234
+ }
1235
+ }
1236
+ ping(now) {
1237
+ const n = this.pingId = this.pingId + 1 >>> 0;
1238
+ if (this.endpoint.sendControl({ t: "ping", e: this.epoch, n })) {
1239
+ this.pendingPings.set(n, now);
1240
+ this.lastPing = now;
1241
+ }
1242
+ for (const [id, at] of this.pendingPings) if (now - at > 1e4) this.pendingPings.delete(id);
1243
+ }
1244
+ sendEvent(data) {
1245
+ return this.endpoint.sendEvent(data);
1246
+ }
1247
+ /** Queue the newest state for a stream. Returns its seq, or 0 before the session is online and the clock synced. */
1248
+ sendState(stream, data, options) {
1249
+ if (!this.clock.synced) return 0;
1250
+ return this.endpoint.sendState(stream, data, this.clock.serverTime(performance.now()), options);
1251
+ }
1252
+ flush() {
1253
+ this.endpoint.flush();
1254
+ }
1255
+ /** Stop the physical connection, retaining event counters and resume credentials in this instance. */
1256
+ pause() {
1257
+ this.stopped = true;
1258
+ ++this.generation;
1259
+ clearTimeout(this.retryTimer);
1260
+ clearInterval(this.pumpTimer);
1261
+ this.stopRtc(false);
1262
+ this.endpoint.detach();
1263
+ this.socket?.close(4003, "paused");
1264
+ this.socket = void 0;
1265
+ this.setStatus("paused");
1266
+ }
1267
+ resume() {
1268
+ if (this.status === "paused") this.connect();
1269
+ }
1270
+ /** Explicitly discard the old logical session after expiry. Pending actions are NOT replayed into a new session. */
1271
+ newSession() {
1272
+ this.pause();
1273
+ this.endpoint = this.makeEndpoint();
1274
+ this.token = this.id = null;
1275
+ this.epoch = 0;
1276
+ this.connect();
1277
+ }
1278
+ close() {
1279
+ if (this.status === "closed") return;
1280
+ try {
1281
+ this.endpoint.sendControl({ t: "leave", e: this.epoch });
1282
+ } catch {
1283
+ }
1284
+ this.pause();
1285
+ this.lifecycle?.();
1286
+ this.lifecycle = void 0;
1287
+ this.setStatus("closed");
1288
+ }
1289
+ checkpoint() {
1290
+ return this.token && this.id ? { url: this.options.url, room: this.options.room, token: this.token, id: this.id, events: this.endpoint.events.checkpoint() } : null;
1291
+ }
1292
+ /** Opt-in page lifecycle integration: pause when the page goes away or stays hidden, reconnect fast when it is back,
1293
+ * and replace a socket that died while the phone slept. Returns cleanup. */
1294
+ bindPageLifecycle(o = {}) {
1295
+ this.lifecycle?.();
1296
+ const hiddenPause = o.hiddenPauseMs ?? 2e4, wakeCheck = o.wakeCheckMs ?? 1500;
1297
+ let hideTimer;
1298
+ const pauseNow = () => {
1299
+ clearTimeout(hideTimer);
1300
+ if (this.status === "online" || this.status === "connecting") this.pause();
1301
+ };
1302
+ const back = () => {
1303
+ clearTimeout(hideTimer);
1304
+ if (this.status === "paused") {
1305
+ this.resume();
1306
+ return;
1307
+ }
1308
+ if (this.status === "online") {
1309
+ this.wakeDeadline = performance.now() + wakeCheck;
1310
+ this.ping(performance.now());
1311
+ } else this.reconnectNow();
1312
+ };
1313
+ const visible = () => {
1314
+ if (document.visibilityState === "visible") back();
1315
+ else {
1316
+ clearTimeout(hideTimer);
1317
+ hideTimer = setTimeout(pauseNow, hiddenPause);
1318
+ }
1319
+ };
1320
+ const pagehide = () => pauseNow(), pageshow = () => back(), online = () => this.reconnectNow();
1321
+ window.addEventListener("pagehide", pagehide);
1322
+ window.addEventListener("pageshow", pageshow);
1323
+ window.addEventListener("online", online);
1324
+ document.addEventListener("visibilitychange", visible);
1325
+ const off = () => {
1326
+ clearTimeout(hideTimer);
1327
+ window.removeEventListener("pagehide", pagehide);
1328
+ window.removeEventListener("pageshow", pageshow);
1329
+ window.removeEventListener("online", online);
1330
+ document.removeEventListener("visibilitychange", visible);
1331
+ };
1332
+ this.lifecycle = off;
1333
+ return off;
1334
+ }
1335
+ };
1336
+
1337
+ // src/core/lobby.ts
1338
+ var LOBBY_KEY = "$lobby";
1339
+ function normalizeCode(code) {
1340
+ if (typeof code !== "string") return null;
1341
+ const c = code.trim().toUpperCase();
1342
+ return /^[A-Z0-9]{3,12}$/.test(c) ? c : null;
1343
+ }
1344
+ var range = (a, b) => String.fromCharCode(a) + "-" + String.fromCharCode(b);
1345
+ var INVISIBLE = new RegExp("[" + [range(0, 31), range(127, 159), range(8203, 8207), range(8232, 8238), range(8294, 8297)].join("") + "]", "g");
1346
+ function cleanName(name, max = 16) {
1347
+ if (typeof name !== "string") return "";
1348
+ return name.replace(/\s+/g, " ").replace(INVISIBLE, "").replace(/ +/g, " ").trim().slice(0, max);
1349
+ }
1350
+ function isLobbyMessage(d) {
1351
+ return !!d && typeof d === "object" && !Array.isArray(d) && typeof d[LOBBY_KEY] === "string";
1352
+ }
1353
+
1354
+ // src/client/lobby.ts
1355
+ var LobbyClient = class {
1356
+ constructor(client, requestTimeoutMs = 2e4) {
1357
+ __publicField(this, "client", client);
1358
+ __publicField(this, "requestTimeoutMs", requestTimeoutMs);
1359
+ __publicField(this, "room", null);
1360
+ __publicField(this, "onRoom", () => {
1361
+ });
1362
+ /** The room was left without asking (kicked, replaced, closed, session expired). */
1363
+ __publicField(this, "onLeft", () => {
1364
+ });
1365
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
1366
+ __publicField(this, "nextId", 1);
1367
+ __publicField(this, "off");
1368
+ this.off = [
1369
+ client.intercept((d) => {
1370
+ if (!isLobbyMessage(d)) return false;
1371
+ this.message(d);
1372
+ return true;
1373
+ }),
1374
+ client.on("session", ({ resumed }) => {
1375
+ if (!resumed) this.drop("new session");
1376
+ }),
1377
+ client.on("lost", ({ reason }) => this.drop(reason))
1378
+ ];
1379
+ }
1380
+ dispose() {
1381
+ for (const f of this.off) f();
1382
+ this.drop("disposed", false);
1383
+ }
1384
+ get me() {
1385
+ return this.room?.members.find((m) => m.seat === this.room.you) ?? null;
1386
+ }
1387
+ get isHost() {
1388
+ return !!this.me?.host;
1389
+ }
1390
+ /** Milliseconds until the ready countdown ends, or null when none is running. */
1391
+ countdownMs() {
1392
+ const end = this.room?.countdownEndsAt;
1393
+ if (end == null || !this.client.clock.synced) return null;
1394
+ return Math.max(0, end - this.client.clock.serverTime(performance.now()));
1395
+ }
1396
+ create(o = {}) {
1397
+ return this.request({ [LOBBY_KEY]: "create", ...o });
1398
+ }
1399
+ join(code, o = {}) {
1400
+ const c = normalizeCode(code);
1401
+ if (!c) return Promise.reject(new Error("That is not a room code"));
1402
+ return this.request({ [LOBBY_KEY]: "join", code: c, ...o });
1403
+ }
1404
+ quickPlay(o = {}) {
1405
+ return this.request({ [LOBBY_KEY]: "quick", ...o });
1406
+ }
1407
+ async leave() {
1408
+ await this.request({ [LOBBY_KEY]: "leave" });
1409
+ this.set(null);
1410
+ }
1411
+ setReady(ready) {
1412
+ return this.request({ [LOBBY_KEY]: "ready", ready });
1413
+ }
1414
+ setName(name) {
1415
+ return this.request({ [LOBBY_KEY]: "name", name });
1416
+ }
1417
+ setMeta(meta) {
1418
+ return this.request({ [LOBBY_KEY]: "meta", meta });
1419
+ }
1420
+ kick(seat) {
1421
+ return this.request({ [LOBBY_KEY]: "kick", seat });
1422
+ }
1423
+ request(body) {
1424
+ const id = this.nextId++;
1425
+ return new Promise((resolve, reject) => {
1426
+ const timer = setTimeout(() => {
1427
+ this.pending.delete(id);
1428
+ reject(new Error("The room server did not answer"));
1429
+ }, this.requestTimeoutMs);
1430
+ this.pending.set(id, { resolve, reject, timer });
1431
+ try {
1432
+ this.client.sendEvent({ ...body, id });
1433
+ } catch (e) {
1434
+ clearTimeout(timer);
1435
+ this.pending.delete(id);
1436
+ reject(e instanceof Error ? e : new Error(String(e)));
1437
+ }
1438
+ });
1439
+ }
1440
+ set(room) {
1441
+ this.room = room;
1442
+ try {
1443
+ this.onRoom(room);
1444
+ } catch {
1445
+ }
1446
+ }
1447
+ message(d) {
1448
+ const op = d[LOBBY_KEY];
1449
+ if (op === "res" && typeof d.id === "number") {
1450
+ const p = this.pending.get(d.id);
1451
+ if (!p) return;
1452
+ this.pending.delete(d.id);
1453
+ clearTimeout(p.timer);
1454
+ if (d.ok === true) {
1455
+ const room = d.room ?? null;
1456
+ if (room || this.room) this.set(room);
1457
+ p.resolve(room);
1458
+ } else p.reject(new Error(typeof d.error === "string" ? d.error : "Request refused"));
1459
+ return;
1460
+ }
1461
+ if (op === "room" && d.room && typeof d.room === "object") {
1462
+ this.set(d.room);
1463
+ return;
1464
+ }
1465
+ if (op === "left") {
1466
+ const had = !!this.room;
1467
+ this.set(null);
1468
+ if (had) try {
1469
+ this.onLeft(typeof d.reason === "string" ? d.reason : "left");
1470
+ } catch {
1471
+ }
1472
+ }
1473
+ }
1474
+ drop(reason, notify = true) {
1475
+ for (const p of this.pending.values()) {
1476
+ clearTimeout(p.timer);
1477
+ p.reject(new Error(reason));
1478
+ }
1479
+ this.pending.clear();
1480
+ if (this.room) {
1481
+ this.set(null);
1482
+ if (notify) try {
1483
+ this.onLeft(reason);
1484
+ } catch {
1485
+ }
1486
+ }
1487
+ }
1488
+ };
1489
+
1490
+ // src/netcode/timing.ts
1491
+ var FixedStepper = class {
1492
+ constructor(hz = 30, maxCatchUp = 4) {
1493
+ __publicField(this, "maxCatchUp", maxCatchUp);
1494
+ __publicField(this, "tick", 0);
1495
+ __publicField(this, "overruns", 0);
1496
+ __publicField(this, "droppedMs", 0);
1497
+ __publicField(this, "accumulator", 0);
1498
+ __publicField(this, "dtMs");
1499
+ if (!(hz > 0) || !Number.isFinite(hz) || !Number.isInteger(maxCatchUp) || maxCatchUp < 1) throw new RangeError("Invalid tick policy");
1500
+ this.dtMs = 1e3 / hz;
1501
+ }
1502
+ advance(elapsedMs, step) {
1503
+ if (!Number.isFinite(elapsedMs) || elapsedMs < 0) throw new RangeError("Invalid elapsed time");
1504
+ this.accumulator += elapsedMs;
1505
+ let n = 0;
1506
+ while (this.accumulator + 1e-8 >= this.dtMs && n < this.maxCatchUp) {
1507
+ this.accumulator -= this.dtMs;
1508
+ this.tick++;
1509
+ n++;
1510
+ step(this.dtMs / 1e3, this.tick);
1511
+ }
1512
+ if (this.accumulator >= this.dtMs) {
1513
+ const discard = Math.floor(this.accumulator / this.dtMs) * this.dtMs;
1514
+ this.accumulator -= discard;
1515
+ this.droppedMs += discard;
1516
+ this.overruns++;
1517
+ }
1518
+ return n;
1519
+ }
1520
+ get alpha() {
1521
+ return Math.max(0, this.accumulator / this.dtMs);
1522
+ }
1523
+ };
1524
+ var PROFILES = Object.freeze({
1525
+ social: { simulationHz: 20, sendHz: 20, bytesPerSecond: 12 * 1024, interpolationMinMs: 50 },
1526
+ action: { simulationHz: 60, sendHz: 30, bytesPerSecond: 24 * 1024, interpolationMinMs: 35 },
1527
+ strategy: { simulationHz: 10, sendHz: 10, bytesPerSecond: 12 * 1024, interpolationMinMs: 100 },
1528
+ turnBased: { simulationHz: 0, sendHz: 0, bytesPerSecond: 8 * 1024, interpolationMinMs: 0 }
1529
+ });
1530
+
1531
+ // src/netcode/interpolation.ts
1532
+ var Timeline = class {
1533
+ constructor(capacity = 64, clone = (v) => structuredClone(v)) {
1534
+ __publicField(this, "capacity", capacity);
1535
+ __publicField(this, "clone", clone);
1536
+ __publicField(this, "samples", []);
1537
+ if (!Number.isInteger(capacity) || capacity < 2 || capacity > 4096) throw new RangeError("Invalid history capacity");
1538
+ }
1539
+ get size() {
1540
+ return this.samples.length;
1541
+ }
1542
+ clear() {
1543
+ this.samples = [];
1544
+ }
1545
+ insert(sample) {
1546
+ if (!Number.isFinite(sample.time) || !Number.isSafeInteger(sample.seq) || sample.seq < 0) throw new RangeError("Invalid snapshot");
1547
+ if (this.samples.some((s) => s.seq === sample.seq)) return false;
1548
+ const same2 = this.samples.findIndex((s) => s.time === sample.time);
1549
+ if (same2 >= 0) {
1550
+ if (this.samples[same2].seq > sample.seq) return false;
1551
+ this.samples.splice(same2, 1);
1552
+ }
1553
+ const copy = { ...sample, value: this.clone(sample.value) };
1554
+ const i = this.samples.findIndex((s) => s.time > sample.time);
1555
+ if (i < 0) this.samples.push(copy);
1556
+ else this.samples.splice(i, 0, copy);
1557
+ while (this.samples.length > this.capacity) this.samples.shift();
1558
+ return true;
1559
+ }
1560
+ sample(at, interpolate, extrapolate, maxExtrapolationMs = 100) {
1561
+ if (!Number.isFinite(at) || !Number.isFinite(maxExtrapolationMs) || maxExtrapolationMs < 0) throw new RangeError("Invalid sample time");
1562
+ const s = this.samples;
1563
+ if (!s.length || s[0].time > at) return null;
1564
+ let i = s.length - 1;
1565
+ while (i > 0 && s[i].time > at) i--;
1566
+ const a = s[i], b = s[i + 1];
1567
+ if (b && !b.discontinuity) return { value: interpolate(this.clone(a.value), this.clone(b.value), (at - a.time) / (b.time - a.time)), mode: "interpolate", sourceTime: a.time };
1568
+ if (!b && extrapolate && i > 0 && at > a.time && !a.discontinuity) {
1569
+ const p = s[i - 1];
1570
+ return { value: extrapolate(this.clone(a.value), this.clone(p.value), Math.min(at - a.time, maxExtrapolationMs), a.time - p.time), mode: "extrapolate", sourceTime: a.time };
1571
+ }
1572
+ return { value: this.clone(a.value), mode: "hold", sourceTime: a.time };
1573
+ }
1574
+ };
1575
+ var AdaptivePlayback = class {
1576
+ constructor(intervalMs, minMs = intervalMs, maxMs = 400) {
1577
+ __publicField(this, "intervalMs", intervalMs);
1578
+ __publicField(this, "minMs", minMs);
1579
+ __publicField(this, "maxMs", maxMs);
1580
+ __publicField(this, "lateness", []);
1581
+ __publicField(this, "cursor", -Infinity);
1582
+ __publicField(this, "delayMs");
1583
+ if (!(intervalMs > 0) || minMs < 0 || maxMs < minMs) throw new RangeError("Invalid playback policy");
1584
+ this.delayMs = minMs;
1585
+ }
1586
+ observe(arrivalOnServerClock, sampleTime) {
1587
+ const late = arrivalOnServerClock - sampleTime;
1588
+ if (!Number.isFinite(late) || late < -1e3 || late > 3e4) return;
1589
+ this.lateness.push(Math.max(0, late));
1590
+ if (this.lateness.length > 128) this.lateness.shift();
1591
+ }
1592
+ advance(serverNow, dtMs) {
1593
+ if (!Number.isFinite(serverNow) || !Number.isFinite(dtMs) || dtMs < 0) throw new RangeError("Invalid clock");
1594
+ if (this.lateness.length) {
1595
+ const sorted = [...this.lateness].sort((a, b) => a - b);
1596
+ const p95 = sorted[Math.floor((sorted.length - 1) * 0.95)];
1597
+ const target = Math.max(this.minMs, Math.min(this.maxMs, p95 + this.intervalMs));
1598
+ const rate = target > this.delayMs ? 8 : 0.5;
1599
+ this.delayMs += (target - this.delayMs) * (1 - Math.exp(-rate * dtMs / 1e3));
1600
+ }
1601
+ this.cursor = Math.max(this.cursor, serverNow - this.delayMs);
1602
+ return this.cursor;
1603
+ }
1604
+ reset() {
1605
+ this.lateness = [];
1606
+ this.cursor = -Infinity;
1607
+ this.delayMs = this.minMs;
1608
+ }
1609
+ };
1610
+ var lerp = (a, b, t) => a + (b - a) * t;
1611
+ var lerpAngle = (a, b, t) => a + Math.atan2(Math.sin(b - a), Math.cos(b - a)) * t;
1612
+ var lerpVec3 = (a, b, t) => ({ x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t), z: lerp(a.z, b.z, t) });
1613
+ function rewindTime(requested, now, maxRewindMs) {
1614
+ if (![requested, now, maxRewindMs].every(Number.isFinite) || maxRewindMs < 0) throw new RangeError("Invalid rewind");
1615
+ return Math.max(now - maxRewindMs, Math.min(now, requested));
1616
+ }
1617
+
1618
+ // src/netcode/prediction.ts
1619
+ var Predictor = class {
1620
+ constructor(initial, step, dtSeconds = 1 / 60, maxPending = 256, cloneState = (v) => structuredClone(v), cloneInput = (v) => structuredClone(v)) {
1621
+ __publicField(this, "step", step);
1622
+ __publicField(this, "dtSeconds", dtSeconds);
1623
+ __publicField(this, "maxPending", maxPending);
1624
+ __publicField(this, "cloneState", cloneState);
1625
+ __publicField(this, "cloneInput", cloneInput);
1626
+ __publicField(this, "state");
1627
+ __publicField(this, "pending", []);
1628
+ __publicField(this, "seq", 0);
1629
+ __publicField(this, "ack", 0);
1630
+ __publicField(this, "serverTick", -1);
1631
+ if (!(dtSeconds > 0) || !Number.isFinite(dtSeconds) || !Number.isInteger(maxPending) || maxPending < 1) throw new RangeError("Invalid prediction options");
1632
+ this.state = cloneState(initial);
1633
+ }
1634
+ get lastInput() {
1635
+ return this.seq;
1636
+ }
1637
+ get pendingCount() {
1638
+ return this.pending.length;
1639
+ }
1640
+ push(value) {
1641
+ if (this.pending.length >= this.maxPending) throw new Error("Prediction history full: resync required");
1642
+ if (this.seq >= 4294967295) throw new Error("Input sequence exhausted: resync required");
1643
+ const frame = { seq: ++this.seq, value: this.cloneInput(value) };
1644
+ this.pending.push(frame);
1645
+ this.state = this.step(this.cloneState(this.state), this.cloneInput(value), this.dtSeconds);
1646
+ return { seq: frame.seq, value: this.cloneInput(frame.value) };
1647
+ }
1648
+ reconcile(snapshot) {
1649
+ const { ack, tick } = snapshot;
1650
+ if (!Number.isSafeInteger(ack) || ack < 0 || ack > this.seq || !Number.isSafeInteger(tick) || tick < 0) throw new Error("Invalid authoritative snapshot");
1651
+ if (tick <= this.serverTick) return false;
1652
+ if (ack < this.ack) throw new Error("Authoritative acknowledgement regressed");
1653
+ this.serverTick = tick;
1654
+ this.ack = ack;
1655
+ this.pending = this.pending.filter((i) => i.seq > ack);
1656
+ let state = this.cloneState(snapshot.state);
1657
+ for (const i of this.pending) state = this.step(state, this.cloneInput(i.value), this.dtSeconds);
1658
+ this.state = state;
1659
+ return true;
1660
+ }
1661
+ recent(count = 3) {
1662
+ return (count <= 0 ? [] : this.pending.slice(-Math.floor(count))).map((i) => ({ seq: i.seq, value: this.cloneInput(i.value) }));
1663
+ }
1664
+ resync(state, tick, ack = 0) {
1665
+ if (!Number.isSafeInteger(tick) || tick < 0 || !Number.isSafeInteger(ack) || ack < 0 || ack > 4294967295) throw new RangeError("Invalid resync");
1666
+ this.state = this.cloneState(state);
1667
+ this.pending = [];
1668
+ this.serverTick = tick;
1669
+ this.ack = this.seq = ack;
1670
+ }
1671
+ };
1672
+ var InputInbox = class {
1673
+ constructor(capacity = 256, clone = (v) => structuredClone(v)) {
1674
+ __publicField(this, "capacity", capacity);
1675
+ __publicField(this, "clone", clone);
1676
+ __publicField(this, "applied", 0);
1677
+ __publicField(this, "queued", /* @__PURE__ */ new Map());
1678
+ }
1679
+ add(input) {
1680
+ if (!Number.isSafeInteger(input.seq) || input.seq <= this.applied || input.seq > this.applied + this.capacity || this.queued.has(input.seq)) return false;
1681
+ this.queued.set(input.seq, this.clone(input.value));
1682
+ return true;
1683
+ }
1684
+ next() {
1685
+ const seq = this.applied + 1;
1686
+ if (!this.queued.has(seq)) return null;
1687
+ const value = this.queued.get(seq);
1688
+ this.queued.delete(seq);
1689
+ this.applied = seq;
1690
+ return { seq, value };
1691
+ }
1692
+ /** Explicit game policy after an expired input gap. Never called automatically. */
1693
+ skipThrough(seq) {
1694
+ if (!Number.isSafeInteger(seq) || seq < this.applied || seq > this.applied + this.capacity) throw new RangeError("Invalid input skip");
1695
+ for (const n of this.queued.keys()) if (n <= seq) this.queued.delete(n);
1696
+ this.applied = seq;
1697
+ }
1698
+ };
1699
+
1700
+ // src/netcode/spatial.ts
1701
+ var SpatialGrid = class {
1702
+ constructor(cellSize = 32, maxEntities = 1e4) {
1703
+ __publicField(this, "cellSize", cellSize);
1704
+ __publicField(this, "maxEntities", maxEntities);
1705
+ __publicField(this, "items", /* @__PURE__ */ new Map());
1706
+ __publicField(this, "cells", /* @__PURE__ */ new Map());
1707
+ if (!(cellSize > 0) || !Number.isFinite(cellSize)) throw new RangeError("Invalid cell size");
1708
+ }
1709
+ validate(p) {
1710
+ if (![p.x, p.y].every((v) => Number.isFinite(v) && Math.abs(v) <= 1e8)) throw new RangeError("Invalid position");
1711
+ }
1712
+ upsert(id, p) {
1713
+ this.validate(p);
1714
+ if (!this.items.has(id) && this.items.size >= this.maxEntities) throw new Error("Spatial entity limit");
1715
+ const key = `${Math.floor(p.x / this.cellSize)},${Math.floor(p.y / this.cellSize)}`;
1716
+ const old = this.items.get(id);
1717
+ if (old && old.key !== key) {
1718
+ const c2 = this.cells.get(old.key);
1719
+ c2.delete(id);
1720
+ if (!c2.size) this.cells.delete(old.key);
1721
+ }
1722
+ let c = this.cells.get(key);
1723
+ if (!c) this.cells.set(key, c = /* @__PURE__ */ new Set());
1724
+ c.add(id);
1725
+ this.items.set(id, { ...p, key });
1726
+ }
1727
+ remove(id) {
1728
+ const p = this.items.get(id);
1729
+ if (!p) return;
1730
+ this.items.delete(id);
1731
+ const c = this.cells.get(p.key);
1732
+ c.delete(id);
1733
+ if (!c.size) this.cells.delete(p.key);
1734
+ }
1735
+ query(center, radius) {
1736
+ this.validate(center);
1737
+ if (!Number.isFinite(radius) || radius < 0) throw new RangeError("Invalid radius");
1738
+ const minX = Math.floor((center.x - radius) / this.cellSize), maxX = Math.floor((center.x + radius) / this.cellSize);
1739
+ const minY = Math.floor((center.y - radius) / this.cellSize), maxY = Math.floor((center.y + radius) / this.cellSize);
1740
+ if ((maxX - minX + 1) * (maxY - minY + 1) > 4096) throw new RangeError("Query exceeds cell budget");
1741
+ const found = /* @__PURE__ */ new Set(), r2 = radius * radius;
1742
+ for (let x = minX; x <= maxX; x++) for (let y = minY; y <= maxY; y++) {
1743
+ for (const id of this.cells.get(`${x},${y}`) ?? []) {
1744
+ const p = this.items.get(id);
1745
+ if ((p.x - center.x) ** 2 + (p.y - center.y) ** 2 <= r2) found.add(id);
1746
+ }
1747
+ }
1748
+ return found;
1749
+ }
1750
+ };
1751
+
1752
+ // src/netcode/codec.ts
1753
+ var sizes = { u8: 1, i8: 1, u16: 2, i16: 2, u32: 4, f32: 4, f64: 8 };
1754
+ var bounds = {
1755
+ u8: [0, 255],
1756
+ i8: [-128, 127],
1757
+ u16: [0, 65535],
1758
+ i16: [-32768, 32767],
1759
+ u32: [0, 4294967295],
1760
+ f32: [-34028234663852886e22, 34028234663852886e22],
1761
+ f64: [-Number.MAX_VALUE, Number.MAX_VALUE]
1762
+ };
1763
+ function defineSchema(schema) {
1764
+ const fields = Object.entries(schema).map(([key, f]) => ({ key, type: f.type, scale: f.scale ?? 1 }));
1765
+ for (const f of fields) if (!Object.hasOwn(sizes, f.type) || !(f.scale > 0) || !Number.isFinite(f.scale) || ["__proto__", "prototype", "constructor"].includes(f.key)) throw new RangeError("Invalid field");
1766
+ const byteLength = fields.reduce((n, f) => n + sizes[f.type], 0);
1767
+ if (!byteLength || byteLength > 1078) throw new RangeError("Schema must fit one state payload");
1768
+ return {
1769
+ byteLength,
1770
+ encode(value) {
1771
+ const b = new Uint8Array(byteLength), v = new DataView(b.buffer);
1772
+ let at = 0;
1773
+ for (const f of fields) {
1774
+ const source = value[f.key];
1775
+ if (!Number.isFinite(source)) throw new RangeError(`Invalid ${f.key}`);
1776
+ let n = source * f.scale;
1777
+ if (!f.type.startsWith("f")) n = Math.round(n);
1778
+ const [min, max] = bounds[f.type];
1779
+ if (!Number.isFinite(n) || n < min || n > max) throw new RangeError(`Out of range: ${f.key}`);
1780
+ switch (f.type) {
1781
+ case "u8":
1782
+ v.setUint8(at, n);
1783
+ break;
1784
+ case "i8":
1785
+ v.setInt8(at, n);
1786
+ break;
1787
+ case "u16":
1788
+ v.setUint16(at, n, true);
1789
+ break;
1790
+ case "i16":
1791
+ v.setInt16(at, n, true);
1792
+ break;
1793
+ case "u32":
1794
+ v.setUint32(at, n, true);
1795
+ break;
1796
+ case "f32":
1797
+ v.setFloat32(at, n, true);
1798
+ break;
1799
+ case "f64":
1800
+ v.setFloat64(at, n, true);
1801
+ break;
1802
+ }
1803
+ at += sizes[f.type];
1804
+ }
1805
+ return b;
1806
+ },
1807
+ decode(b) {
1808
+ if (b.length !== byteLength) return null;
1809
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength), out = /* @__PURE__ */ Object.create(null);
1810
+ let at = 0;
1811
+ for (const f of fields) {
1812
+ let n;
1813
+ switch (f.type) {
1814
+ case "u8":
1815
+ n = v.getUint8(at);
1816
+ break;
1817
+ case "i8":
1818
+ n = v.getInt8(at);
1819
+ break;
1820
+ case "u16":
1821
+ n = v.getUint16(at, true);
1822
+ break;
1823
+ case "i16":
1824
+ n = v.getInt16(at, true);
1825
+ break;
1826
+ case "u32":
1827
+ n = v.getUint32(at, true);
1828
+ break;
1829
+ case "f32":
1830
+ n = v.getFloat32(at, true);
1831
+ break;
1832
+ case "f64":
1833
+ n = v.getFloat64(at, true);
1834
+ break;
1835
+ }
1836
+ if (!Number.isFinite(n) || !Number.isFinite(n / f.scale)) return null;
1837
+ out[f.key] = n / f.scale;
1838
+ at += sizes[f.type];
1839
+ }
1840
+ return out;
1841
+ }
1842
+ };
1843
+ }
1844
+
1845
+ // src/netcode/replication.ts
1846
+ var sameBytes = (a, b) => a.length === b.length && a.every((v, i) => b[i] === v);
1847
+ var ReplicaSender = class {
1848
+ constructor(maxEntities = 1024, retryMs = 100) {
1849
+ __publicField(this, "maxEntities", maxEntities);
1850
+ __publicField(this, "retryMs", retryMs);
1851
+ __publicField(this, "records", /* @__PURE__ */ new Map());
1852
+ }
1853
+ publish(id, generation, data) {
1854
+ if (!isUint(id, 65535) || !isUint(generation) || generation < 1 || (data?.length ?? 0) > 1067) throw new RangeError("Invalid entity");
1855
+ const old = this.records.get(id), deleted = data === null, bytes = data?.slice() ?? new Uint8Array();
1856
+ if (!old && this.records.size >= this.maxEntities) throw new Error("Replica entity limit");
1857
+ if (old && generation < old.update.generation) throw new Error("Entity generation regressed");
1858
+ if (old && generation === old.update.generation && old.update.deleted === deleted && sameBytes(old.update.data, bytes)) return { id, generation, revision: old.update.revision };
1859
+ const revision = old && generation === old.update.generation ? old.update.revision + 1 : 1;
1860
+ if (revision > 4294967295) throw new Error("Entity revision exhausted");
1861
+ const update = { id, generation, revision, deleted, data: bytes };
1862
+ this.records.set(id, { update, ack: old && generation === old.update.generation ? old.ack : 0, sent: 0, sentAt: -Infinity });
1863
+ return { id, generation, revision };
1864
+ }
1865
+ due(now, limit = 64) {
1866
+ return [...this.records.values()].filter((r) => r.ack < r.update.revision && now - r.sentAt >= this.retryMs).sort((a, b) => a.sentAt - b.sentAt).slice(0, limit).map((r) => ({ ...r.update, data: r.update.data.slice() }));
1867
+ }
1868
+ markSent(v, now) {
1869
+ const r = this.records.get(v.id);
1870
+ if (!r || r.update.generation !== v.generation || r.update.revision !== v.revision) return;
1871
+ r.sent = v.revision;
1872
+ r.sentAt = now;
1873
+ }
1874
+ acknowledge(v) {
1875
+ const r = this.records.get(v.id);
1876
+ if (!r || r.update.generation !== v.generation || !isUint(v.revision) || v.revision > r.sent) return false;
1877
+ r.ack = Math.max(r.ack, v.revision);
1878
+ return true;
1879
+ }
1880
+ /** Reset both sender and receiver under a new world/session generation, not while old packets remain valid. */
1881
+ clear() {
1882
+ this.records.clear();
1883
+ }
1884
+ };
1885
+ var ReplicaReceiver = class {
1886
+ constructor(maxEntities = 1024) {
1887
+ __publicField(this, "maxEntities", maxEntities);
1888
+ __publicField(this, "records", /* @__PURE__ */ new Map());
1889
+ }
1890
+ apply(u) {
1891
+ if (!isUint(u.id, 65535) || !isUint(u.generation) || !u.generation || !isUint(u.revision) || !u.revision || u.data.length > 1067) throw new Error("Invalid entity update");
1892
+ const old = this.records.get(u.id);
1893
+ if (!old && this.records.size >= this.maxEntities) throw new Error("Replica receiver full");
1894
+ const changed = !old || u.generation > old.generation || u.generation === old.generation && u.revision > old.revision;
1895
+ if (changed) this.records.set(u.id, { ...u, data: u.data.slice() });
1896
+ return { changed, ack: { id: u.id, generation: u.generation, revision: u.revision } };
1897
+ }
1898
+ get(id) {
1899
+ const r = this.records.get(id);
1900
+ return r && !r.deleted ? r.data.slice() : null;
1901
+ }
1902
+ clear() {
1903
+ this.records.clear();
1904
+ }
1905
+ };
1906
+ function encodeReplica(u) {
1907
+ if (!isUint(u.id, 65535) || !isUint(u.generation) || !u.generation || !isUint(u.revision) || !u.revision || u.data.length > 1067 || u.deleted && u.data.length)
1908
+ throw new Error("Invalid entity update");
1909
+ const b = new Uint8Array(11 + u.data.length), v = new DataView(b.buffer);
1910
+ v.setUint16(0, u.id, true);
1911
+ v.setUint32(2, u.generation, true);
1912
+ v.setUint32(6, u.revision, true);
1913
+ v.setUint8(10, +u.deleted);
1914
+ b.set(u.data, 11);
1915
+ return b;
1916
+ }
1917
+ function decodeReplica(b) {
1918
+ if (b.length < 11 || b.length > 1078) return null;
1919
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength), flag = v.getUint8(10), generation = v.getUint32(2, true), revision = v.getUint32(6, true);
1920
+ if (flag > 1 || !generation || !revision || flag && b.length !== 11) return null;
1921
+ return { id: v.getUint16(0, true), generation, revision, deleted: !!flag, data: b.slice(11) };
1922
+ }
1923
+
1924
+ // src/netcode/inputs.ts
1925
+ function encodeInputs(inputs) {
1926
+ if (!inputs.length || inputs.length > 32) throw new RangeError("Invalid input count");
1927
+ let size = 1, previous = -1;
1928
+ for (const input of inputs) {
1929
+ if (!isUint(input.seq) || input.seq <= previous || input.data.length > 1024) throw new RangeError("Invalid input");
1930
+ previous = input.seq;
1931
+ size += 6 + input.data.length;
1932
+ }
1933
+ if (size > MAX_STATE_BYTES) throw new RangeError("Input bundle exceeds one state frame");
1934
+ const b = new Uint8Array(size), v = new DataView(b.buffer);
1935
+ b[0] = inputs.length;
1936
+ let at = 1;
1937
+ for (const i of inputs) {
1938
+ v.setUint32(at, i.seq, true);
1939
+ v.setUint16(at + 4, i.data.length, true);
1940
+ b.set(i.data, at + 6);
1941
+ at += 6 + i.data.length;
1942
+ }
1943
+ return b;
1944
+ }
1945
+ function decodeInputs(b) {
1946
+ if (!b.length || b.length > MAX_STATE_BYTES || !b[0] || b[0] > 32) return null;
1947
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength), out = [];
1948
+ let at = 1, previous = -1;
1949
+ for (let n = 0; n < b[0]; n++) {
1950
+ if (at + 6 > b.length) return null;
1951
+ const seq = v.getUint32(at, true), size = v.getUint16(at + 4, true);
1952
+ if (seq <= previous || size > 1024 || at + 6 + size > b.length) return null;
1953
+ out.push({ seq, data: b.slice(at + 6, at + 6 + size) });
1954
+ previous = seq;
1955
+ at += 6 + size;
1956
+ }
1957
+ return at === b.length ? out : null;
1958
+ }
1959
+
1960
+ // src/netcode/snapshots.ts
1961
+ var SnapshotHistory = class {
1962
+ constructor(capacity = 64) {
1963
+ __publicField(this, "capacity", capacity);
1964
+ __publicField(this, "sent", /* @__PURE__ */ new Map());
1965
+ __publicField(this, "acked", null);
1966
+ if (!Number.isInteger(capacity) || capacity < 1) throw new RangeError("Invalid history capacity");
1967
+ }
1968
+ record(seq, value) {
1969
+ if (!seq) return;
1970
+ this.sent.set(seq, value);
1971
+ while (this.sent.size > this.capacity) this.sent.delete(this.sent.keys().next().value);
1972
+ }
1973
+ acknowledge(seq) {
1974
+ const value = this.sent.get(seq);
1975
+ if (value === void 0 || this.acked && !newer(seq, this.acked.seq)) return false;
1976
+ this.acked = { seq, value };
1977
+ for (const k of [...this.sent.keys()]) if (!newer(k, seq)) this.sent.delete(k);
1978
+ return true;
1979
+ }
1980
+ /** The acknowledged snapshot to diff against, or null for a whole snapshot. */
1981
+ get baseline() {
1982
+ return this.acked;
1983
+ }
1984
+ reset() {
1985
+ this.sent.clear();
1986
+ this.acked = null;
1987
+ }
1988
+ };
1989
+ var same = (a, b) => a === b || typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
1990
+ function diffEntities(base, next) {
1991
+ const out = {};
1992
+ for (const id of Object.keys(next)) {
1993
+ const n = next[id], b = base?.[id];
1994
+ let fields;
1995
+ for (const k of Object.keys(n)) if (!b || !same(b[k], n[k])) (fields ?? (fields = {}))[k] = n[k];
1996
+ if (b) {
1997
+ for (const k of Object.keys(b)) if (!(k in n)) (fields ?? (fields = {}))[k] = null;
1998
+ }
1999
+ if (fields) (out.set ?? (out.set = {}))[id] = fields;
2000
+ }
2001
+ if (base) {
2002
+ for (const id of Object.keys(base)) if (!(id in next)) (out.del ?? (out.del = [])).push(id);
2003
+ }
2004
+ return out;
2005
+ }
2006
+ function applyEntityDelta(base, delta) {
2007
+ const out = {};
2008
+ if (base) for (const id of Object.keys(base)) out[id] = { ...base[id] };
2009
+ for (const id of delta.del ?? []) delete out[id];
2010
+ for (const [id, fields] of Object.entries(delta.set ?? {})) {
2011
+ const e = out[id] ?? (out[id] = {});
2012
+ for (const [k, v] of Object.entries(fields)) {
2013
+ if (v === null && base?.[id] && k in base[id]) delete e[k];
2014
+ else e[k] = v;
2015
+ }
2016
+ }
2017
+ return out;
2018
+ }
2019
+ export {
2020
+ AdaptivePlayback,
2021
+ BrowserRtcPeer,
2022
+ DEFAULT_STATE_BYTES,
2023
+ FixedStepper,
2024
+ GameClient,
2025
+ InputInbox,
2026
+ LOBBY_KEY,
2027
+ LobbyClient,
2028
+ MTU_STATE_BYTES,
2029
+ NetworkClock,
2030
+ PROFILES,
2031
+ Predictor,
2032
+ ReplicaReceiver,
2033
+ ReplicaSender,
2034
+ SnapshotHistory,
2035
+ SpatialGrid,
2036
+ Timeline,
2037
+ VERSION,
2038
+ applyEntityDelta,
2039
+ cleanName,
2040
+ decodeInputs,
2041
+ decodeReplica,
2042
+ defineSchema,
2043
+ diffEntities,
2044
+ encodeInputs,
2045
+ encodeReplica,
2046
+ lerp,
2047
+ lerpAngle,
2048
+ lerpVec3,
2049
+ normalizeCode,
2050
+ rewindTime
2051
+ };
2052
+ //# sourceMappingURL=twinwire.mjs.map