@irtio/protocol 0.5.1 → 0.6.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 (3) hide show
  1. package/dist/index.d.ts +1095 -9
  2. package/dist/index.js +889 -39
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/frame.ts
2
- var PROTOCOL_VERSION = 1;
2
+ var PROTOCOL_VERSION = 2;
3
3
  var FrameType = {
4
4
  HELLO: 1,
5
5
  WELCOME: 2,
@@ -18,7 +18,19 @@ var FrameType = {
18
18
  * reconnect grace window and the room's `onLeave` sees `reason: 'left'` instead of waiting for
19
19
  * the close to time out into `'timeout'`. Additive — protocol version stays 1.
20
20
  */
21
- LEAVE: 12
21
+ LEAVE: 12,
22
+ /**
23
+ * Server → client, payload the new schema's canonical JSON as UTF-8 (D50, gap-and-resync).
24
+ * Sent during a `migrate`-strategy deploy whose schema change is purely additive, after the old
25
+ * worker stops sending and before the resync WELCOME, to a session that asked for it. The
26
+ * client rebuilds its codec from the descriptor and keeps its socket; before this, any schema
27
+ * change closed it with `E_SCHEMA_MISMATCH`.
28
+ *
29
+ * Gated, not versioned. `decodeFrame` throws on an unknown type, so a client that predates this
30
+ * frame must never receive one: the server sends it only to a session whose HELLO set
31
+ * `HELLO_SCHEMA_SWAP_BIT`. Additive — protocol version stays 1, the same way LEAVE=12 did.
32
+ */
33
+ SCHEMA: 13
22
34
  };
23
35
  var FRAME_TYPE_VALUES = new Set(Object.values(FrameType));
24
36
  function isFrameType(v) {
@@ -45,7 +57,7 @@ var ErrorCode = {
45
57
  E_SCHEMA_MISMATCH: { code: 4, message: "schema hash mismatch" },
46
58
  E_ROOM_NOT_FOUND: {
47
59
  code: 5,
48
- message: "room {roomId} not found \u2014 room ids are 1-32 of A-Z a-z 0-9 - _"
60
+ message: "room {roomId} not found. Room ids are 1-32 of A-Z a-z 0-9 - _, optionally behind a lowercase <type>: prefix"
49
61
  },
50
62
  E_ROOM_FULL: { code: 6, message: "room {roomId} is full" },
51
63
  E_ROOM_CLOSED: { code: 7, message: "room {roomId} is closed" },
@@ -74,8 +86,72 @@ var ErrorCode = {
74
86
  E_TOKEN_WRONG_ROOM: { code: 24, message: "auth token is for another room" },
75
87
  E_TOKEN_MALFORMED: { code: 25, message: "auth token is malformed: {reason}" },
76
88
  E_TOKEN_BAD_ISSUER: { code: 26, message: "auth token issuer {issuer} is not accepted" },
77
- E_TOKEN_BAD_ALG: { code: 27, message: "auth token algorithm {alg} is not allowed" }
89
+ E_TOKEN_BAD_ALG: { code: 27, message: "auth token algorithm {alg} is not allowed" },
90
+ /**
91
+ * M4 (D48): the project is at a usage cap and this join is new work.
92
+ *
93
+ * The template carries `{detail}` rather than a fixed sentence because the actionable half
94
+ * differs by tier and by meter, and a refusal that does not say what to do next is the thing
95
+ * `errors.md`'s fix-line convention exists to prevent. Control composes the detail; the box
96
+ * relays it. Non-protocol surfaces (the store gateway's write refusal, control's HTTP refusals
97
+ * for deploys and uploads) reuse the same `E_USAGE_CAP` string code in their own shapes, so one
98
+ * grep finds every place a cap can be met.
99
+ */
100
+ E_USAGE_CAP: { code: 28, message: "{detail}" },
101
+ /**
102
+ * M4 part 5 (D53): the tenant cannot verify platform identity assertions because it holds no
103
+ * platform public key yet.
104
+ *
105
+ * Deliberately not `E_AUTH` and not `E_TOKEN_INVALID`. Those say "your credential is bad", and
106
+ * this is the opposite — the credential may be perfect and the box is not yet in a position to
107
+ * say so. Keys are delivered to a tenant the way cap state is (a push from control right after
108
+ * the VM comes up), so the honest reading of this code is "retry in a moment", which is what
109
+ * the fix line in the errors reference says.
110
+ */
111
+ E_ASSERTION_UNVERIFIABLE: {
112
+ code: 29,
113
+ message: "this server cannot verify identity assertions yet"
114
+ },
115
+ /**
116
+ * M5 part 3.5: a room type is already running as many rooms as it declared it would.
117
+ *
118
+ * Deliberately its own code rather than `E_ROOM_FULL`, which is about clients in a room, and
119
+ * emphatically not `E_INTERNAL`. This refusal is what makes declared sizing honest: the tenant's
120
+ * VM memory was computed as the declared per-room heap times this number, so room N+1 is a room
121
+ * the machine was never built to hold. The message names the type, the limit and the field to
122
+ * change, because all three are things the developer controls.
123
+ */
124
+ E_TYPE_AT_CAPACITY: { code: 30, message: "{detail}" },
125
+ /**
126
+ * M5 part 3.5 (Part C): the project has used its egress allowance and forwarding has stopped.
127
+ *
128
+ * Its own code, and its own WebSocket close code ({@link CLOSE_EGRESS_WALL}), for one reason: a
129
+ * developer watching sockets close has to be able to tell a wall from a bug. `E_USAGE_CAP` is
130
+ * the polite refusal of *new* work at a cap control evaluated up to a minute ago; this is the
131
+ * box stopping traffic on its own, locally, the moment its leased budget ran out.
132
+ */
133
+ E_EGRESS_WALL: { code: 31, message: "{detail}" },
134
+ /**
135
+ * M5 part 3.5 (Part C): a free-tier shape limit — the relay client cap, or the concurrent
136
+ * awake-rooms cap. The message names the limit and the tier, because the fix is a card.
137
+ */
138
+ E_TIER_LIMIT: { code: 32, message: "{detail}" },
139
+ /**
140
+ * M5 part 7 (D67-i): the project's region has no server with room for another tenant VM.
141
+ *
142
+ * Sent by the router, fatal, with close code 1013 (RFC 6455 "try again later"). Before this
143
+ * code a full region looked like a slow start: the control plane re-ran placement on every
144
+ * lookup poll for thirty seconds and the client then saw `E_STARTING` followed by `E_INTERNAL`.
145
+ * The message names the region and the two fixes because both are an operator's, never the
146
+ * player's: the client does not retry this join on its own.
147
+ */
148
+ E_PLACEMENT: {
149
+ code: 33,
150
+ message: "no capacity in region {region}; add a server or raise maxVms"
151
+ }
78
152
  };
153
+ var CLOSE_TRY_AGAIN_LATER = 1013;
154
+ var CLOSE_EGRESS_WALL = 4290;
79
155
  var ERROR_CATALOGUE = Object.keys(ErrorCode).map((name) => ({ name, code: ErrorCode[name].code, message: ErrorCode[name].message }));
80
156
  var BY_CODE = new Map(
81
157
  ERROR_CATALOGUE.map((e) => [e.code, e])
@@ -101,6 +177,7 @@ function assertEof(r, what) {
101
177
  var HELLO_RESUME_BIT = 1 << 0;
102
178
  var HELLO_ROLE_BIT = 1 << 1;
103
179
  var HELLO_NAME_BIT = 1 << 2;
180
+ var HELLO_SCHEMA_SWAP_BIT = 1 << 3;
104
181
  function encodeHello(h) {
105
182
  if (h.schemaHash8.length !== 8) {
106
183
  throw new Error(`encodeHello: schemaHash8 must be 8 bytes, got ${h.schemaHash8.length}`);
@@ -114,7 +191,7 @@ function encodeHello(h) {
114
191
  w.u8(1);
115
192
  w.str(h.credential.token);
116
193
  } else {
117
- w.u8(2);
194
+ w.u8(h.credential.kind === "assertion" ? 3 : 2);
118
195
  w.str(h.credential.key);
119
196
  w.str(h.credential.token);
120
197
  }
@@ -124,6 +201,7 @@ function encodeHello(h) {
124
201
  if (h.resumeToken !== void 0) mask |= HELLO_RESUME_BIT;
125
202
  if (h.role !== void 0) mask |= HELLO_ROLE_BIT;
126
203
  if (h.name !== void 0) mask |= HELLO_NAME_BIT;
204
+ if (h.schemaSwap === true) mask |= HELLO_SCHEMA_SWAP_BIT;
127
205
  w.u8(mask);
128
206
  if (h.resumeToken !== void 0) w.str(h.resumeToken);
129
207
  if (h.role !== void 0) w.str(h.role);
@@ -138,6 +216,7 @@ function decodeHello(bytes) {
138
216
  if (credKind === 0) credential = { kind: "key", key: r.str() };
139
217
  else if (credKind === 1) credential = { kind: "token", token: r.str() };
140
218
  else if (credKind === 2) credential = { kind: "jwt", key: r.str(), token: r.str() };
219
+ else if (credKind === 3) credential = { kind: "assertion", key: r.str(), token: r.str() };
141
220
  else throw new Error(`decodeHello: unknown credential kind ${credKind}`);
142
221
  const roomId = r.str();
143
222
  const schemaHash8 = r.bytes(8);
@@ -145,13 +224,15 @@ function decodeHello(bytes) {
145
224
  const resumeToken = (mask & HELLO_RESUME_BIT) !== 0 ? r.str() : void 0;
146
225
  const role = (mask & HELLO_ROLE_BIT) !== 0 ? r.str() : void 0;
147
226
  const name = (mask & HELLO_NAME_BIT) !== 0 ? r.str() : void 0;
227
+ const schemaSwap = (mask & HELLO_SCHEMA_SWAP_BIT) !== 0;
148
228
  assertEof(r, "decodeHello");
149
229
  const hello = { protocolVersion, credential, roomId, schemaHash8 };
150
230
  return {
151
231
  ...hello,
152
232
  ...resumeToken !== void 0 ? { resumeToken } : {},
153
233
  ...role !== void 0 ? { role } : {},
154
- ...name !== void 0 ? { name } : {}
234
+ ...name !== void 0 ? { name } : {},
235
+ ...schemaSwap ? { schemaSwap: true } : {}
155
236
  };
156
237
  }
157
238
  function encodeWelcome(w0) {
@@ -162,7 +243,8 @@ function encodeWelcome(w0) {
162
243
  w.blob(w0.snapshot);
163
244
  w.str(w0.resumeToken);
164
245
  w.str(w0.roomId);
165
- w.u16(w0.tickIntervalMs);
246
+ w.u16(w0.tickRate);
247
+ w.u16(w0.maxClients);
166
248
  return w.finish();
167
249
  }
168
250
  function decodeWelcome(bytes) {
@@ -173,9 +255,10 @@ function decodeWelcome(bytes) {
173
255
  const snapshot = r.blob();
174
256
  const resumeToken = r.str();
175
257
  const roomId = r.str();
176
- const tickIntervalMs = r.eof ? 0 : r.u16();
258
+ const tickRate = r.eof ? 0 : r.u16();
259
+ const maxClients = r.eof ? 0 : r.u16();
177
260
  assertEof(r, "decodeWelcome");
178
- return { clientId, role, tick, snapshot, resumeToken, roomId, tickIntervalMs };
261
+ return { clientId, role, tick, snapshot, resumeToken, roomId, tickRate, maxClients };
179
262
  }
180
263
  function encodeErrorPayload(e) {
181
264
  const w = new ByteWriter();
@@ -245,6 +328,15 @@ function encodeWriteFrame(codecBytes) {
245
328
  function encodeCorrectFrame(codecBytes, clientTick, appliedTick) {
246
329
  return encodeFrame(FrameType.CORRECT, correctPayload(codecBytes, clientTick, appliedTick));
247
330
  }
331
+ function schemaPayload(canonical) {
332
+ return new TextEncoder().encode(canonical);
333
+ }
334
+ function readSchemaPayload(payload) {
335
+ return new TextDecoder("utf-8", { fatal: true }).decode(payload);
336
+ }
337
+ function encodeSchemaFrame(canonical) {
338
+ return encodeFrame(FrameType.SCHEMA, schemaPayload(canonical));
339
+ }
248
340
 
249
341
  // src/rpc.ts
250
342
  import { ByteReader as ByteReader2, ByteWriter as ByteWriter2, bool, server, str } from "@irtio/schema";
@@ -323,12 +415,422 @@ function rpcByIdOf(schema, id) {
323
415
  return found;
324
416
  }
325
417
 
418
+ // src/profile.ts
419
+ import { ByteReader as ByteReader3, walkDelta, walkSnapshot } from "@irtio/schema";
420
+
421
+ // src/presence.ts
422
+ import { bool as bool2, defineSchema, entity, str as str2 } from "@irtio/schema";
423
+ var presenceEntity = entity(
424
+ { clientId: str2(32), role: str2(32), name: str2(32), connected: bool2 },
425
+ { serverOwned: true }
426
+ );
427
+ var PRESENCE_COLLECTION = "clients";
428
+ var extended = /* @__PURE__ */ new WeakMap();
429
+ function withBuiltins(schema) {
430
+ let ext = extended.get(schema);
431
+ if (!ext) {
432
+ ext = defineSchema(
433
+ { ...schema.defs, [PRESENCE_COLLECTION]: presenceEntity },
434
+ {
435
+ rpc: schema.rpc,
436
+ roles: schema.roles,
437
+ ...schema.project !== void 0 ? { project: schema.project } : {},
438
+ allowReservedNames: true
439
+ }
440
+ );
441
+ extended.set(schema, ext);
442
+ }
443
+ return ext;
444
+ }
445
+ var relaySchema = withBuiltins(defineSchema({}));
446
+ var RELAY_HASH8 = new Uint8Array(8);
447
+ function isRelayHash8(hash8) {
448
+ if (hash8.length !== 8) return false;
449
+ for (const b of hash8) if (b !== 0) return false;
450
+ return true;
451
+ }
452
+
453
+ // src/profile.ts
454
+ var PROFILE_KINDS = [
455
+ "field",
456
+ "presence",
457
+ "correction",
458
+ "write",
459
+ "churn",
460
+ "rpc",
461
+ "message",
462
+ "voice",
463
+ "join",
464
+ "overhead",
465
+ "control"
466
+ ];
467
+ var EMPTY_PROFILE = {
468
+ rows: [],
469
+ bytesIn: 0,
470
+ bytesOut: 0,
471
+ framesIn: 0,
472
+ framesOut: 0,
473
+ walks: 0
474
+ };
475
+ var CONTROL_KEYS = {
476
+ [FrameType.HELLO]: "hello",
477
+ [FrameType.WELCOME]: "welcome",
478
+ [FrameType.ERROR]: "error",
479
+ [FrameType.PING]: "ping",
480
+ [FrameType.PONG]: "pong",
481
+ [FrameType.LEAVE]: "leave",
482
+ [FrameType.SCHEMA]: "schema"
483
+ };
484
+ var MSG_KEYS = ["all", "client", "role", "server"];
485
+ var PENDING_CALLS_MAX = 512;
486
+ var ProfileLedger = class {
487
+ rows = /* @__PURE__ */ new Map();
488
+ memo = /* @__PURE__ */ new Map();
489
+ /** Peer and reqId to the rpc name its CALL carried, so the matching REPLY can be named too. */
490
+ pending = /* @__PURE__ */ new Map();
491
+ scratch = [];
492
+ bytesIn = 0;
493
+ bytesOut = 0;
494
+ framesIn = 0;
495
+ framesOut = 0;
496
+ walkCount = 0;
497
+ rpcSchema;
498
+ /**
499
+ * `schema` is the runtime-extended schema (`withBuiltins`), which is what delta and snapshot
500
+ * bodies are encoded against. `rpcSchema` is the one whose `rpcTable` the `rpcId` on the wire
501
+ * indexes; the two tables agree today (extending adds a collection, not an rpc), but the
502
+ * senders name them separately and so does this.
503
+ */
504
+ schema;
505
+ constructor(schema, rpcSchema) {
506
+ this.schema = schema;
507
+ this.rpcSchema = rpcSchema ?? schema;
508
+ }
509
+ /**
510
+ * D50: a session whose schema was swapped mid-flight keeps its ledger and its accumulated rows.
511
+ * Rows named after a field the new schema dropped simply stop growing, which is the honest
512
+ * reading — those bytes really were spent.
513
+ */
514
+ swapSchema(schema, rpcSchema) {
515
+ this.schema = schema;
516
+ this.rpcSchema = rpcSchema ?? schema;
517
+ this.memo.clear();
518
+ }
519
+ get walks() {
520
+ return this.walkCount;
521
+ }
522
+ /** Drops the shared-payload memo. Call at the top of each flush. */
523
+ newFlush() {
524
+ if (this.memo.size > 0) this.memo.clear();
525
+ }
526
+ reset() {
527
+ this.rows.clear();
528
+ this.memo.clear();
529
+ this.pending.clear();
530
+ this.bytesIn = 0;
531
+ this.bytesOut = 0;
532
+ this.framesIn = 0;
533
+ this.framesOut = 0;
534
+ this.walkCount = 0;
535
+ }
536
+ snapshot() {
537
+ const rows = [];
538
+ for (const kind of PROFILE_KINDS) {
539
+ const byKey = this.rows.get(kind);
540
+ if (!byKey) continue;
541
+ for (const [key, v] of byKey) rows.push({ kind, key, out: v.out, in: v.in });
542
+ }
543
+ rows.sort((a, b) => b.out + b.in - (a.out + a.in) || (a.key < b.key ? -1 : 1));
544
+ return {
545
+ rows,
546
+ bytesIn: this.bytesIn,
547
+ bytesOut: this.bytesOut,
548
+ framesIn: this.framesIn,
549
+ framesOut: this.framesOut,
550
+ walks: this.walkCount
551
+ };
552
+ }
553
+ /**
554
+ * Attributes one whole frame, envelope byte included. Never throws: a frame this ledger cannot
555
+ * read still contributes its full length, to `overhead/unattributed`.
556
+ */
557
+ attribute(dir, frame, options) {
558
+ if (dir === "in") {
559
+ this.framesIn++;
560
+ this.bytesIn += frame.length;
561
+ } else {
562
+ this.framesOut++;
563
+ this.bytesOut += frame.length;
564
+ }
565
+ const shared = options?.shared;
566
+ if (shared !== void 0) {
567
+ const cached = this.memo.get(shared);
568
+ if (cached) {
569
+ this.commit(dir, cached);
570
+ return;
571
+ }
572
+ }
573
+ const entries = shared !== void 0 ? [] : this.scratch;
574
+ entries.length = 0;
575
+ this.walkCount++;
576
+ this.describe(frame, entries, options);
577
+ const total = sum(entries);
578
+ if (total !== frame.length) {
579
+ entries.push({
580
+ kind: "overhead",
581
+ key: "unattributed",
582
+ bytes: frame.length - total
583
+ });
584
+ }
585
+ if (shared !== void 0) this.memo.set(shared, entries);
586
+ this.commit(dir, entries);
587
+ }
588
+ /**
589
+ * Attributes bare snapshot bytes under `join`. The room worker needs this: it hands the host
590
+ * snapshot bytes and the host wraps them in a `WELCOME`, so the room never sees that frame,
591
+ * but the bytes are still the room's egress and the join rows are the reason anyone profiles a
592
+ * join at all. A client, which does see the whole frame, uses `attribute` instead.
593
+ */
594
+ attributeSnapshot(dir, bytes) {
595
+ if (dir === "in") {
596
+ this.bytesIn += bytes.length;
597
+ } else {
598
+ this.bytesOut += bytes.length;
599
+ }
600
+ const entries = [];
601
+ this.walkCount++;
602
+ try {
603
+ this.snapshotBody(bytes, entries);
604
+ } catch {
605
+ entries.length = 0;
606
+ }
607
+ const total = sum(entries);
608
+ if (total !== bytes.length) {
609
+ entries.push({ kind: "overhead", key: "unattributed", bytes: bytes.length - total });
610
+ }
611
+ this.commit(dir, entries);
612
+ }
613
+ // -------------------------------------------------------------------------
614
+ commit(dir, entries) {
615
+ for (const e of entries) {
616
+ if (e.bytes === 0) continue;
617
+ let byKey = this.rows.get(e.kind);
618
+ if (!byKey) {
619
+ byKey = /* @__PURE__ */ new Map();
620
+ this.rows.set(e.kind, byKey);
621
+ }
622
+ let row = byKey.get(e.key);
623
+ if (!row) {
624
+ row = { out: 0, in: 0 };
625
+ byKey.set(e.key, row);
626
+ }
627
+ if (dir === "in") row.in += e.bytes;
628
+ else row.out += e.bytes;
629
+ }
630
+ }
631
+ describe(frame, out, options) {
632
+ if (frame.length === 0) return;
633
+ out.push({ kind: "overhead", key: "envelope", bytes: 1 });
634
+ const type = frame[0];
635
+ const payload = frame.subarray(1);
636
+ try {
637
+ switch (type) {
638
+ case FrameType.DELTA:
639
+ this.body(payload, out, "field", options?.churn, options?.spatialChurn === true);
640
+ return;
641
+ case FrameType.WRITE:
642
+ this.body(payload, out, "write", void 0);
643
+ return;
644
+ case FrameType.CORRECT: {
645
+ const r = new ByteReader3(payload);
646
+ this.body(r, out, "correction", void 0);
647
+ const tail = payload.length - r.pos;
648
+ if (tail > 0) out.push({ kind: "correction", key: "tail", bytes: tail });
649
+ return;
650
+ }
651
+ case FrameType.CALL: {
652
+ const r = new ByteReader3(payload);
653
+ const reqId = r.u32();
654
+ const rpcId = r.u16();
655
+ const name = this.rpcName(rpcId);
656
+ this.remember(options?.peer, reqId, name);
657
+ out.push({ kind: "rpc", key: name, bytes: payload.length });
658
+ return;
659
+ }
660
+ case FrameType.REPLY: {
661
+ const r = new ByteReader3(payload);
662
+ const reqId = r.u32();
663
+ out.push({ kind: "rpc", key: this.recall(options?.peer, reqId), bytes: payload.length });
664
+ return;
665
+ }
666
+ case FrameType.MSG: {
667
+ const kind = payload[0];
668
+ if (kind === 4) out.push({ kind: "voice", key: "signal", bytes: payload.length });
669
+ else if (kind !== void 0 && kind < MSG_KEYS.length) {
670
+ out.push({ kind: "message", key: MSG_KEYS[kind], bytes: payload.length });
671
+ }
672
+ return;
673
+ }
674
+ case FrameType.WELCOME:
675
+ this.welcome(payload, out);
676
+ return;
677
+ default: {
678
+ const key = CONTROL_KEYS[type];
679
+ if (key !== void 0) out.push({ kind: "control", key, bytes: payload.length });
680
+ return;
681
+ }
682
+ }
683
+ } catch {
684
+ }
685
+ }
686
+ /** WELCOME: the snapshot inside it is `join`; everything around it is `control/welcome`. */
687
+ welcome(payload, out) {
688
+ const r = new ByteReader3(payload);
689
+ skipStr(r);
690
+ skipStr(r);
691
+ r.u32();
692
+ const len = r.varint();
693
+ const before = r.pos;
694
+ if (r.remaining < len) throw new Error("welcome: short snapshot");
695
+ const consumed = this.snapshotBody(payload.subarray(before, before + len), out);
696
+ out.push({ kind: "control", key: "welcome", bytes: payload.length - consumed });
697
+ }
698
+ snapshotBody(bytes, out) {
699
+ const collect = this.collector(out, "join", "join", "header", void 0);
700
+ return walkSnapshot(this.schema, bytes, collect);
701
+ }
702
+ body(bytes, out, fieldKind, churn, spatialChurn = false) {
703
+ const collect = this.collector(out, fieldKind, "overhead", "delta-header", churn, spatialChurn);
704
+ walkDelta(this.schema, bytes, collect);
705
+ }
706
+ /**
707
+ * The walker's consumer. `op` latches: a churn op routes its own framing *and* every field
708
+ * that follows it into `churn`, because a synthetic add carries a whole record whose bytes are
709
+ * the cost of the entity crossing the boundary, not the cost of the fields changing.
710
+ */
711
+ collector(out, fieldKind, overheadKind, headerKey, churn, spatialChurn = false) {
712
+ let churnCollection;
713
+ return {
714
+ header: (bytes) => {
715
+ out.push({ kind: overheadKind, key: headerKey, bytes });
716
+ },
717
+ op: (c, kind, id, bytes) => {
718
+ const isChurn = kind !== "update" && (spatialChurn ? c.visibility === "spatial-grid" : churn !== void 0 && churn.get(c.name)?.has(id) === true);
719
+ churnCollection = isChurn ? c.name : void 0;
720
+ out.push(
721
+ churnCollection !== void 0 ? { kind: "churn", key: c.name, bytes } : { kind: overheadKind, key: overheadKind === "join" ? headerKey : "op", bytes }
722
+ );
723
+ },
724
+ field: (c, f, bytes) => {
725
+ if (churnCollection !== void 0) {
726
+ out.push({ kind: "churn", key: churnCollection, bytes });
727
+ return;
728
+ }
729
+ const key = `${c.name}.${f.name}`;
730
+ out.push({
731
+ kind: fieldKind === "field" && c.name === PRESENCE_COLLECTION ? "presence" : fieldKind,
732
+ key,
733
+ bytes
734
+ });
735
+ }
736
+ };
737
+ }
738
+ rpcName(rpcId) {
739
+ try {
740
+ return rpcByIdOf(this.rpcSchema, rpcId).name;
741
+ } catch {
742
+ return `rpc#${rpcId}`;
743
+ }
744
+ }
745
+ remember(peer, reqId, name) {
746
+ if (this.pending.size >= PENDING_CALLS_MAX) this.pending.clear();
747
+ this.pending.set(`${peer ?? ""}\0${reqId}`, name);
748
+ }
749
+ recall(peer, reqId) {
750
+ const k = `${peer ?? ""}\0${reqId}`;
751
+ const name = this.pending.get(k);
752
+ if (name === void 0) return "(unmatched reply)";
753
+ this.pending.delete(k);
754
+ return name;
755
+ }
756
+ };
757
+ function sum(entries) {
758
+ let n = 0;
759
+ for (const e of entries) n += e.bytes;
760
+ return n;
761
+ }
762
+ function skipStr(r) {
763
+ const n = r.varint();
764
+ if (r.remaining < n) throw new Error("unexpected end of buffer");
765
+ r.pos += n;
766
+ }
767
+ function diffProfiles(prev, next) {
768
+ const before = /* @__PURE__ */ new Map();
769
+ for (const r of prev.rows) before.set(`${r.kind} ${r.key}`, r);
770
+ const rows = [];
771
+ for (const r of next.rows) {
772
+ const b = before.get(`${r.kind} ${r.key}`);
773
+ const out = r.out - (b?.out ?? 0);
774
+ const inb = r.in - (b?.in ?? 0);
775
+ if (out !== 0 || inb !== 0) rows.push({ kind: r.kind, key: r.key, out, in: inb });
776
+ }
777
+ rows.sort((a, b) => b.out + b.in - (a.out + a.in) || (a.key < b.key ? -1 : 1));
778
+ return {
779
+ rows,
780
+ bytesIn: next.bytesIn - prev.bytesIn,
781
+ bytesOut: next.bytesOut - prev.bytesOut,
782
+ framesIn: next.framesIn - prev.framesIn,
783
+ framesOut: next.framesOut - prev.framesOut,
784
+ walks: next.walks - prev.walks
785
+ };
786
+ }
787
+ function topProfileRows(snap, n) {
788
+ return [...snap.rows].sort((a, b) => b.out + b.in - (a.out + a.in)).slice(0, n);
789
+ }
790
+ function scaleProfile(snap, n) {
791
+ if (n <= 1) return snap;
792
+ return {
793
+ rows: snap.rows.map((r) => ({ kind: r.kind, key: r.key, out: r.out / n, in: r.in / n })),
794
+ bytesIn: snap.bytesIn / n,
795
+ bytesOut: snap.bytesOut / n,
796
+ framesIn: snap.framesIn / n,
797
+ framesOut: snap.framesOut / n,
798
+ walks: snap.walks
799
+ };
800
+ }
801
+ function mergeProfiles(a, b) {
802
+ const byKey = /* @__PURE__ */ new Map();
803
+ for (const r of [...a.rows, ...b.rows]) {
804
+ const k = `${r.kind} ${r.key}`;
805
+ const row = byKey.get(k);
806
+ if (row) {
807
+ row.out += r.out;
808
+ row.in += r.in;
809
+ } else byKey.set(k, { kind: r.kind, key: r.key, out: r.out, in: r.in });
810
+ }
811
+ const rows = [...byKey.values()].sort(
812
+ (x, y) => y.out + y.in - (x.out + x.in) || (x.key < y.key ? -1 : 1)
813
+ );
814
+ return {
815
+ rows,
816
+ bytesIn: a.bytesIn + b.bytesIn,
817
+ bytesOut: a.bytesOut + b.bytesOut,
818
+ framesIn: a.framesIn + b.framesIn,
819
+ framesOut: a.framesOut + b.framesOut,
820
+ walks: a.walks + b.walks
821
+ };
822
+ }
823
+
326
824
  // src/msg.ts
327
- import { ByteReader as ByteReader3, ByteWriter as ByteWriter3 } from "@irtio/schema";
825
+ import { ByteReader as ByteReader4, ByteWriter as ByteWriter3 } from "@irtio/schema";
328
826
  var MSG_KIND_ALL = 0;
329
827
  var MSG_KIND_CLIENT = 1;
330
828
  var MSG_KIND_ROLE = 2;
331
829
  var MSG_KIND_SERVER = 3;
830
+ var MSG_KIND_VOICE = 4;
831
+ function isVoiceMsg(payload) {
832
+ return payload.length > 0 && payload[0] === MSG_KIND_VOICE;
833
+ }
332
834
  function encodeMsg(m) {
333
835
  const w = new ByteWriter3();
334
836
  switch (m.target.kind) {
@@ -346,12 +848,15 @@ function encodeMsg(m) {
346
848
  case "server":
347
849
  w.u8(MSG_KIND_SERVER);
348
850
  break;
851
+ case "voice":
852
+ w.u8(MSG_KIND_VOICE);
853
+ break;
349
854
  }
350
855
  w.bytes(m.payload);
351
856
  return w.finish();
352
857
  }
353
858
  function decodeMsg(bytes) {
354
- const r = new ByteReader3(bytes);
859
+ const r = new ByteReader4(bytes);
355
860
  const kind = r.u8();
356
861
  let target;
357
862
  switch (kind) {
@@ -367,6 +872,9 @@ function decodeMsg(bytes) {
367
872
  case MSG_KIND_SERVER:
368
873
  target = { kind: "server" };
369
874
  break;
875
+ case MSG_KIND_VOICE:
876
+ target = { kind: "voice" };
877
+ break;
370
878
  default:
371
879
  throw new Error(`decodeMsg: unknown target discriminator ${kind}`);
372
880
  }
@@ -374,36 +882,316 @@ function decodeMsg(bytes) {
374
882
  return { target, payload };
375
883
  }
376
884
 
377
- // src/presence.ts
378
- import { bool as bool2, defineSchema, entity, str as str2 } from "@irtio/schema";
379
- var presenceEntity = entity(
380
- { clientId: str2(32), role: str2(32), name: str2(32), connected: bool2 },
381
- { serverOwned: true }
382
- );
383
- var PRESENCE_COLLECTION = "clients";
384
- var extended = /* @__PURE__ */ new WeakMap();
385
- function withBuiltins(schema) {
386
- let ext = extended.get(schema);
387
- if (!ext) {
388
- ext = defineSchema(
389
- { ...schema.defs, [PRESENCE_COLLECTION]: presenceEntity },
390
- {
391
- rpc: schema.rpc,
392
- roles: schema.roles,
393
- ...schema.project !== void 0 ? { project: schema.project } : {},
394
- allowReservedNames: true
395
- }
396
- );
397
- extended.set(schema, ext);
885
+ // src/voice.ts
886
+ var ENC = new TextEncoder();
887
+ var DEC = new TextDecoder();
888
+ function encodeVoiceMessage(m) {
889
+ return ENC.encode(JSON.stringify(m));
890
+ }
891
+ function decodeVoiceMessage(bytes) {
892
+ try {
893
+ const parsed = JSON.parse(DEC.decode(bytes));
894
+ if (parsed === null || typeof parsed !== "object") return void 0;
895
+ if (typeof parsed.t !== "string") return void 0;
896
+ return parsed;
897
+ } catch {
898
+ return void 0;
398
899
  }
399
- return ext;
400
900
  }
401
- var relaySchema = withBuiltins(defineSchema({}));
402
- var RELAY_HASH8 = new Uint8Array(8);
403
- function isRelayHash8(hash8) {
404
- if (hash8.length !== 8) return false;
405
- for (const b of hash8) if (b !== 0) return false;
406
- return true;
901
+
902
+ // src/codes.ts
903
+ var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
904
+ var ROOM_TYPE_DELIMITER = ":";
905
+ var ROOM_TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,23}$/;
906
+ var ROOM_ID_RE = /^(?:[a-z0-9][a-z0-9_-]{0,23}:)?[A-Za-z0-9_-]{1,32}$/;
907
+ var DEFAULT_ROOM_TYPE = "default";
908
+ function parseRoomId(raw) {
909
+ if (!ROOM_ID_RE.test(raw)) return void 0;
910
+ const at = raw.indexOf(ROOM_TYPE_DELIMITER);
911
+ if (at < 0) return { type: void 0, id: raw };
912
+ return { type: raw.slice(0, at), id: raw.slice(at + 1) };
913
+ }
914
+ function formatRoomId(type, id) {
915
+ return type === void 0 ? id : `${type}${ROOM_TYPE_DELIMITER}${id}`;
916
+ }
917
+ var MATCH_CODE_LENGTH = 10;
918
+
919
+ // src/bus.ts
920
+ var BUS_LIMITS = {
921
+ /**
922
+ * Max UTF-8 bytes in one payload, for both verbs.
923
+ *
924
+ * 4 KiB is a quarter of the KV value limit, and it is sized for what the feature is for: a
925
+ * match-ready notice, an invite, a bracket announcement — an id, a couple of names, a reason.
926
+ * A payload is also a *store* cost for `send`, because a mailbox entry persists in the alarm
927
+ * sidecar, so the ceiling on one room's mailbox is this times `mailboxDepth`, and both numbers
928
+ * were picked together.
929
+ */
930
+ payloadBytes: 4 * 1024,
931
+ /** Max UTF-8 bytes in a channel name. Names are vocabulary, not data. */
932
+ channelBytes: 128,
933
+ /** Max channels one room may hold a subscription to at once. */
934
+ subscriptionsPerRoom: 32,
935
+ /**
936
+ * Max undelivered mailbox entries for one target room.
937
+ *
938
+ * This is a store bound as much as a memory one: entries live in the alarm sidecar, so at the
939
+ * payload cap above, a full mailbox is 256 KiB of persisted object. Past this, a `send` is
940
+ * refused at the sender with a named error rather than silently dropped at the target, because
941
+ * a sender that is outrunning a receiver is a fact the sender can act on.
942
+ */
943
+ mailboxDepth: 64,
944
+ /**
945
+ * How many times one mailbox entry may be delivered before it is dead-lettered.
946
+ *
947
+ * The bound exists because at-least-once and the room crash threshold compose badly on purpose.
948
+ * A handler that always throws surfaces as a crash, and the supervisor's window is 3 restarts in
949
+ * 60 seconds; an entry that re-armed forever would close the room in four deliveries, on every
950
+ * wake, for as long as the entry existed. Five attempts is enough to ride out a transient (a
951
+ * deploy mid-flight, a wake that raced a hibernate) and few enough that a genuinely poisonous
952
+ * message costs a room one bad minute rather than its life. What happens then is a log line and
953
+ * a drop: honest for v1, and the docs say so rather than implying a dead-letter queue exists.
954
+ */
955
+ maxDeliveryAttempts: 5,
956
+ /**
957
+ * Bus operations one room may issue per second, averaged, and the burst it may take at once.
958
+ *
959
+ * This is the "one hot room cannot melt the supervisor" guardrail D59 asks for. It counts both
960
+ * verbs together, because both cost the supervisor a fan-out or a store write, and a limiter
961
+ * that only counted the cheap one would be a limiter a room could route around.
962
+ */
963
+ opsPerSecond: 50,
964
+ opsBurst: 100,
965
+ /**
966
+ * How long a delivered-but-unacknowledged mailbox entry stays armed before it is delivered
967
+ * again. A visibility timeout, and it is what makes at-least-once actually true rather than
968
+ * nearly true.
969
+ *
970
+ * Posting a message to a worker is not the same as the worker running it: a room that dies
971
+ * between the post and the handler (a crash, a restart, a hibernate that raced the delivery)
972
+ * would otherwise lose the message with no trace, which is at-most-once wearing at-least-once's
973
+ * name. So an entry is re-armed for this long at the moment it is posted, and cleared only when
974
+ * the worker says the handler ran. 30 seconds is far longer than a handler takes and short
975
+ * enough that a genuinely lost delivery is retried while anyone still cares.
976
+ */
977
+ redeliveryDelayMs: 3e4
978
+ };
979
+ var BUS_CHANNEL_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
980
+ var BUS_ERRORS = {
981
+ badChannel: "E_BUS_BAD_CHANNEL",
982
+ payloadTooLarge: "E_BUS_PAYLOAD_TOO_LARGE",
983
+ tooManySubscriptions: "E_BUS_TOO_MANY_SUBSCRIPTIONS",
984
+ /** The named room does not exist in this tenant, or is a relay room and runs no handlers. */
985
+ noSuchRoom: "E_BUS_NO_SUCH_ROOM",
986
+ mailboxFull: "E_BUS_MAILBOX_FULL",
987
+ rateLimited: "E_BUS_RATE_LIMITED"
988
+ };
989
+ function utf8Bytes(s) {
990
+ return new TextEncoder().encode(s).length;
991
+ }
992
+ function busChannelProblem(channel) {
993
+ if (typeof channel !== "string" || channel === "") {
994
+ return { code: BUS_ERRORS.badChannel, message: "a channel name is required" };
995
+ }
996
+ if (utf8Bytes(channel) > BUS_LIMITS.channelBytes) {
997
+ return {
998
+ code: BUS_ERRORS.badChannel,
999
+ message: `channel name is longer than ${BUS_LIMITS.channelBytes} bytes`
1000
+ };
1001
+ }
1002
+ if (!BUS_CHANNEL_RE.test(channel)) {
1003
+ return {
1004
+ code: BUS_ERRORS.badChannel,
1005
+ message: `illegal channel name ${JSON.stringify(channel)}: lowercase, starts alphanumeric, and made of letters, digits, dots, underscores and dashes`
1006
+ };
1007
+ }
1008
+ return void 0;
1009
+ }
1010
+ function busPayloadProblem(payload) {
1011
+ if (typeof payload !== "string") {
1012
+ return {
1013
+ code: BUS_ERRORS.payloadTooLarge,
1014
+ message: "a bus payload must be a string (serialise your own object)"
1015
+ };
1016
+ }
1017
+ const bytes = utf8Bytes(payload);
1018
+ if (bytes > BUS_LIMITS.payloadBytes) {
1019
+ return {
1020
+ code: BUS_ERRORS.payloadTooLarge,
1021
+ message: `payload is ${bytes} bytes; the limit is ${BUS_LIMITS.payloadBytes}`
1022
+ };
1023
+ }
1024
+ return void 0;
1025
+ }
1026
+ var BUS_MAILBOX_PREFIX = "__bus.";
1027
+ function isMailboxAlarm(name) {
1028
+ return name.startsWith(BUS_MAILBOX_PREFIX);
1029
+ }
1030
+ var BUS_OUTBOX_PREFIX = "__busout.";
1031
+ function isOutboxAlarm(name) {
1032
+ return name.startsWith(BUS_OUTBOX_PREFIX);
1033
+ }
1034
+ var BUS_OUTBOX = {
1035
+ firstRetryMs: 1e3,
1036
+ maxRetryMs: 6e4,
1037
+ ttlMs: 60 * 60 * 1e3
1038
+ };
1039
+ var BUS_SHARD_STARTING = "E_SHARD_STARTING";
1040
+
1041
+ // src/classes.ts
1042
+ var ROOM_MEMORY_MB_MIN = 32;
1043
+ var ROOM_MEMORY_MB_MAX = 1024;
1044
+ var ROOM_CLASS_MAX_MB = {
1045
+ small: 64,
1046
+ medium: 256,
1047
+ large: ROOM_MEMORY_MB_MAX
1048
+ };
1049
+ function roomClassFor(memoryMb) {
1050
+ if (memoryMb === void 0 || !Number.isFinite(memoryMb)) return "small";
1051
+ if (memoryMb <= ROOM_CLASS_MAX_MB.small) return "small";
1052
+ if (memoryMb <= ROOM_CLASS_MAX_MB.medium) return "medium";
1053
+ return "large";
1054
+ }
1055
+ function roomHoursSourceFor(cls) {
1056
+ return cls === "small" ? "room" : cls;
1057
+ }
1058
+ function roomClassOfSource(source) {
1059
+ if (source === "room") return "small";
1060
+ if (source === "medium" || source === "large") return source;
1061
+ return void 0;
1062
+ }
1063
+
1064
+ // src/sizing.ts
1065
+ var VM_MEM_MIB_MIN = 128;
1066
+ var VM_MEM_MIB_MAX = 4096;
1067
+ var SUPERVISOR_BASELINE_MIB = 54;
1068
+ var NATIVE_RESERVE_FRACTION = 8;
1069
+ var PHYSICS_HEADROOM_MIB = 112;
1070
+ var YOUNG_GEN_MB = 32;
1071
+ var DEFAULT_MEMORY_MB = 32;
1072
+ var DEFAULT_MAX_AWAKE = 1;
1073
+ var MAX_AWAKE_MAX = 256;
1074
+ function nativeReserveOf(vmMib) {
1075
+ return Math.floor(vmMib / NATIVE_RESERVE_FRACTION);
1076
+ }
1077
+ function typeFootprintMib(type) {
1078
+ const memoryMb = normalizeMemoryMb(type.memoryMb);
1079
+ const maxAwake = normalizeMaxAwake(type.maxAwake);
1080
+ return (memoryMb + YOUNG_GEN_MB) * maxAwake;
1081
+ }
1082
+ function normalizeMemoryMb(value) {
1083
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return DEFAULT_MEMORY_MB;
1084
+ return Math.floor(value);
1085
+ }
1086
+ function normalizeMaxAwake(value) {
1087
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return DEFAULT_MAX_AWAKE;
1088
+ return Math.min(MAX_AWAKE_MAX, Math.floor(value));
1089
+ }
1090
+ function vmSizeFor(types, options = {}) {
1091
+ const baselineMib = options.baselineMib ?? SUPERVISOR_BASELINE_MIB;
1092
+ let workersMib = 0;
1093
+ let anyPhysics = false;
1094
+ for (const type of types) {
1095
+ workersMib += typeFootprintMib(type);
1096
+ if (type.physics === true) anyPhysics = true;
1097
+ }
1098
+ const physicsHeadroomMib = anyPhysics ? PHYSICS_HEADROOM_MIB : 0;
1099
+ const preReserveMib = workersMib + baselineMib + physicsHeadroomMib;
1100
+ const unclamped = Math.ceil(
1101
+ preReserveMib * NATIVE_RESERVE_FRACTION / (NATIVE_RESERVE_FRACTION - 1)
1102
+ );
1103
+ const vmMib = Math.min(VM_MEM_MIB_MAX, Math.max(VM_MEM_MIB_MIN, unclamped));
1104
+ const clamped = unclamped < VM_MEM_MIB_MIN ? "floor" : unclamped > VM_MEM_MIB_MAX ? "ceiling" : void 0;
1105
+ const parts = types.map(
1106
+ (t) => `${t.type} ${normalizeMemoryMb(t.memoryMb)}+${YOUNG_GEN_MB} MB x ${normalizeMaxAwake(t.maxAwake)}`
1107
+ ).join(", ");
1108
+ const detail = `${vmMib} MiB from ${parts || "no room types"}, supervisor baseline ${baselineMib} MiB` + (physicsHeadroomMib > 0 ? `, physics headroom ${physicsHeadroomMib} MiB` : "") + `, native reserve ${vmMib - preReserveMib} MiB` + (clamped === "floor" ? ` (raised to the ${VM_MEM_MIB_MIN} MiB floor)` : clamped === "ceiling" ? ` (lowered to the ${VM_MEM_MIB_MAX} MiB ceiling)` : "");
1109
+ return {
1110
+ vmMib,
1111
+ workersMib,
1112
+ baselineMib,
1113
+ physicsHeadroomMib,
1114
+ preReserveMib,
1115
+ nativeReserveMib: vmMib - preReserveMib,
1116
+ ...clamped !== void 0 ? { clamped } : {},
1117
+ detail
1118
+ };
1119
+ }
1120
+ var ROOMS_PER_VCPU = {
1121
+ small: 20,
1122
+ medium: 8,
1123
+ large: 5
1124
+ };
1125
+ var CPU_QUOTA_PCT_MIN = 10;
1126
+ var CPU_QUOTA_PCT_MAX = 100;
1127
+ function vmCpuFor(types) {
1128
+ if (types.length === 0) {
1129
+ return {
1130
+ quotaPct: CPU_QUOTA_PCT_MAX,
1131
+ rawPct: CPU_QUOTA_PCT_MAX,
1132
+ undeclared: true,
1133
+ detail: "no room types declared, so no CPU throttle"
1134
+ };
1135
+ }
1136
+ const declared = types.some(
1137
+ (t) => t.memoryMb !== void 0 || t.maxAwake !== void 0 && t.maxAwake > 0
1138
+ );
1139
+ if (!declared) {
1140
+ return {
1141
+ quotaPct: CPU_QUOTA_PCT_MAX,
1142
+ rawPct: CPU_QUOTA_PCT_MAX,
1143
+ undeclared: true,
1144
+ detail: `${types.length} room type(s), none declaring memoryMb or maxAwake, so no CPU throttle`
1145
+ };
1146
+ }
1147
+ let rawPct = 0;
1148
+ const parts = [];
1149
+ for (const type of types) {
1150
+ const cls = roomClassFor(normalizeMemoryMbForClass(type.memoryMb));
1151
+ const maxAwake = normalizeMaxAwake(type.maxAwake);
1152
+ const density = ROOMS_PER_VCPU[cls];
1153
+ const share = maxAwake * CPU_QUOTA_PCT_MAX / density;
1154
+ rawPct += share;
1155
+ parts.push(`${type.type} ${cls} x ${maxAwake} / ${density} per vCPU`);
1156
+ }
1157
+ const rounded = Math.ceil(rawPct);
1158
+ const quotaPct = Math.min(CPU_QUOTA_PCT_MAX, Math.max(CPU_QUOTA_PCT_MIN, rounded));
1159
+ const clamped = rounded < CPU_QUOTA_PCT_MIN ? "floor" : rounded > CPU_QUOTA_PCT_MAX ? "ceiling" : void 0;
1160
+ return {
1161
+ quotaPct,
1162
+ rawPct,
1163
+ undeclared: false,
1164
+ ...clamped !== void 0 ? { clamped } : {},
1165
+ detail: `CPUQuota ${quotaPct}% from ${parts.join(", ")}` + (clamped === "floor" ? ` (raised to the ${CPU_QUOTA_PCT_MIN}% floor)` : clamped === "ceiling" ? ` (lowered to the ${CPU_QUOTA_PCT_MAX}% ceiling from ${rounded}%)` : "")
1166
+ };
1167
+ }
1168
+ function normalizeMemoryMbForClass(value) {
1169
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return void 0;
1170
+ return Math.floor(value);
1171
+ }
1172
+ function declarationsFit(vmMemMib, types, options = {}) {
1173
+ const required = vmSizeFor(types, options);
1174
+ return { fits: required.vmMib <= vmMemMib, required };
1175
+ }
1176
+
1177
+ // src/retention.ts
1178
+ var RETENTION_RE = /^[1-9][0-9]{0,4}(m|h|d)$/;
1179
+ var RETENTION_MIN_MS = 6e4;
1180
+ var RETENTION_MAX_MS = 3650 * 24 * 60 * 60 * 1e3;
1181
+ var UNIT_MS = {
1182
+ m: 6e4,
1183
+ h: 60 * 60 * 1e3,
1184
+ d: 24 * 60 * 60 * 1e3
1185
+ };
1186
+ function parseRetention(raw) {
1187
+ if (typeof raw !== "string") return void 0;
1188
+ if (!RETENTION_RE.test(raw)) return void 0;
1189
+ const unit = raw.slice(-1);
1190
+ const scale = UNIT_MS[unit];
1191
+ if (scale === void 0) return void 0;
1192
+ const ms = Number(raw.slice(0, -1)) * scale;
1193
+ if (ms < RETENTION_MIN_MS || ms > RETENTION_MAX_MS) return void 0;
1194
+ return ms;
407
1195
  }
408
1196
 
409
1197
  // src/origin.ts
@@ -429,14 +1217,54 @@ function parseOriginList(raw) {
429
1217
  return raw.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
430
1218
  }
431
1219
  export {
1220
+ BUS_CHANNEL_RE,
1221
+ BUS_ERRORS,
1222
+ BUS_LIMITS,
1223
+ BUS_MAILBOX_PREFIX,
1224
+ BUS_OUTBOX,
1225
+ BUS_OUTBOX_PREFIX,
1226
+ BUS_SHARD_STARTING,
1227
+ CLOSE_EGRESS_WALL,
1228
+ CLOSE_TRY_AGAIN_LATER,
1229
+ CODE_ALPHABET,
1230
+ CPU_QUOTA_PCT_MAX,
1231
+ CPU_QUOTA_PCT_MIN,
1232
+ DEFAULT_MAX_AWAKE,
1233
+ DEFAULT_MEMORY_MB,
1234
+ DEFAULT_ROOM_TYPE,
1235
+ EMPTY_PROFILE,
432
1236
  ERROR_CATALOGUE,
433
1237
  ErrorCode,
434
1238
  FrameType,
1239
+ HELLO_SCHEMA_SWAP_BIT,
1240
+ MATCH_CODE_LENGTH,
1241
+ MAX_AWAKE_MAX,
1242
+ NATIVE_RESERVE_FRACTION,
1243
+ PHYSICS_HEADROOM_MIB,
435
1244
  PRESENCE_COLLECTION,
1245
+ PROFILE_KINDS,
436
1246
  PROTOCOL_VERSION,
1247
+ ProfileLedger,
437
1248
  RELAY_HASH8,
1249
+ RETENTION_MAX_MS,
1250
+ RETENTION_MIN_MS,
1251
+ RETENTION_RE,
1252
+ ROOMS_PER_VCPU,
1253
+ ROOM_CLASS_MAX_MB,
1254
+ ROOM_ID_RE,
1255
+ ROOM_MEMORY_MB_MAX,
1256
+ ROOM_MEMORY_MB_MIN,
1257
+ ROOM_TYPE_DELIMITER,
1258
+ ROOM_TYPE_RE,
1259
+ SUPERVISOR_BASELINE_MIB,
1260
+ VM_MEM_MIB_MAX,
1261
+ VM_MEM_MIB_MIN,
1262
+ YOUNG_GEN_MB,
438
1263
  builtinRpcs,
1264
+ busChannelProblem,
1265
+ busPayloadProblem,
439
1266
  correctPayload,
1267
+ declarationsFit,
440
1268
  decodeCall,
441
1269
  decodeErrorPayload,
442
1270
  decodeFrame,
@@ -445,8 +1273,10 @@ export {
445
1273
  decodePing,
446
1274
  decodePong,
447
1275
  decodeReply,
1276
+ decodeVoiceMessage,
448
1277
  decodeWelcome,
449
1278
  deltaPayload,
1279
+ diffProfiles,
450
1280
  encodeCall,
451
1281
  encodeCorrectFrame,
452
1282
  encodeDeltaFrame,
@@ -457,23 +1287,43 @@ export {
457
1287
  encodePing,
458
1288
  encodePong,
459
1289
  encodeReply,
1290
+ encodeSchemaFrame,
1291
+ encodeVoiceMessage,
460
1292
  encodeWelcome,
461
1293
  encodeWriteFrame,
462
1294
  errorByCode,
463
1295
  formatError,
1296
+ formatRoomId,
464
1297
  isFrameType,
465
1298
  isLocalhostOrigin,
1299
+ isMailboxAlarm,
1300
+ isOutboxAlarm,
466
1301
  isRelayHash8,
1302
+ isVoiceMsg,
1303
+ mergeProfiles,
1304
+ nativeReserveOf,
467
1305
  originAllowed,
468
1306
  parseOriginList,
1307
+ parseRetention,
1308
+ parseRoomId,
469
1309
  presenceEntity,
470
1310
  readCorrectAppliedTick,
471
1311
  readCorrectClientTick,
1312
+ readSchemaPayload,
472
1313
  relaySchema,
473
1314
  requestOwnership,
1315
+ roomClassFor,
1316
+ roomClassOfSource,
1317
+ roomHoursSourceFor,
474
1318
  rpcByIdOf,
475
1319
  rpcIdOf,
476
1320
  rpcTable,
1321
+ scaleProfile,
1322
+ schemaPayload,
1323
+ topProfileRows,
1324
+ typeFootprintMib,
1325
+ vmCpuFor,
1326
+ vmSizeFor,
477
1327
  withBuiltins,
478
1328
  writePayload
479
1329
  };