@irtio/protocol 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1191 -12
- package/dist/index.js +983 -49
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import * as _irtio_schema from '@irtio/schema';
|
|
2
|
-
import { ByteReader, RpcMap, ServerRpcs, AnySchema, RpcDesc, InstanceOf, Schema, SchemaDefs, SchemaRpc, SchemaRoles } from '@irtio/schema';
|
|
2
|
+
import { ByteReader, RpcMap, ServerRpcs, AnySchema, RpcDesc, InstanceOf, Schema, SchemaDefs, SchemaRpc, SchemaRoles, SchemaMessages } from '@irtio/schema';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Frame envelope: one type byte, then an opaque payload. `encodeFrame`/`decodeFrame` only
|
|
6
6
|
* handle the envelope — payload codecs live in `session.ts`, `rpc.ts`, `msg.ts`.
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Wire protocol version (u8), bumped on any breaking change to framing or payload shapes.
|
|
10
|
+
*
|
|
11
|
+
* 2: `WELCOME`'s tick slot carries the room's tick rate in ticks per second, where version 1
|
|
12
|
+
* carried the interval in whole milliseconds (`b28a325`). The slot kept its width and type, so a
|
|
13
|
+
* version 1 client would read `60` as a 60 ms interval and quietly run a four-times-too-long
|
|
14
|
+
* interpolation delay and a wrong physics timestep.
|
|
15
|
+
*
|
|
16
|
+
* 3: `CALL` carries the caller's `clientTick` between `rpcId` and the params (D72, lag
|
|
17
|
+
* compensation). The params are rest-of-buffer, so an older server reading a version 3 `CALL`
|
|
18
|
+
* would take four bytes of the stamp as the head of the params and decode nonsense. Either bump
|
|
19
|
+
* turns the mismatch into `E_PROTOCOL_VERSION` at join, which is a client bundle that needs
|
|
20
|
+
* redeploying rather than an afternoon of debugging. 2 and 3 landed on separate branches the
|
|
21
|
+
* same day and met at the M6 wave 1 merge; both are spent.
|
|
22
|
+
*/
|
|
23
|
+
declare const PROTOCOL_VERSION = 3;
|
|
10
24
|
declare const FrameType: {
|
|
11
25
|
readonly HELLO: 1;
|
|
12
26
|
readonly WELCOME: 2;
|
|
@@ -26,6 +40,18 @@ declare const FrameType: {
|
|
|
26
40
|
* the close to time out into `'timeout'`. Additive — protocol version stays 1.
|
|
27
41
|
*/
|
|
28
42
|
readonly LEAVE: 12;
|
|
43
|
+
/**
|
|
44
|
+
* Server → client, payload the new schema's canonical JSON as UTF-8 (D50, gap-and-resync).
|
|
45
|
+
* Sent during a `migrate`-strategy deploy whose schema change is purely additive, after the old
|
|
46
|
+
* worker stops sending and before the resync WELCOME, to a session that asked for it. The
|
|
47
|
+
* client rebuilds its codec from the descriptor and keeps its socket; before this, any schema
|
|
48
|
+
* change closed it with `E_SCHEMA_MISMATCH`.
|
|
49
|
+
*
|
|
50
|
+
* Gated, not versioned. `decodeFrame` throws on an unknown type, so a client that predates this
|
|
51
|
+
* frame must never receive one: the server sends it only to a session whose HELLO set
|
|
52
|
+
* `HELLO_SCHEMA_SWAP_BIT`. Additive — protocol version stays 1, the same way LEAVE=12 did.
|
|
53
|
+
*/
|
|
54
|
+
readonly SCHEMA: 13;
|
|
29
55
|
};
|
|
30
56
|
type FrameType = (typeof FrameType)[keyof typeof FrameType];
|
|
31
57
|
interface Frame {
|
|
@@ -66,7 +92,7 @@ declare const ErrorCode: {
|
|
|
66
92
|
};
|
|
67
93
|
readonly E_ROOM_NOT_FOUND: {
|
|
68
94
|
readonly code: 5;
|
|
69
|
-
readonly message: "room {roomId} not found
|
|
95
|
+
readonly message: "room {roomId} not found. Room ids are 1-32 of A-Z a-z 0-9 - _, optionally behind a lowercase <type>: prefix";
|
|
70
96
|
};
|
|
71
97
|
readonly E_ROOM_FULL: {
|
|
72
98
|
readonly code: 6;
|
|
@@ -156,7 +182,125 @@ declare const ErrorCode: {
|
|
|
156
182
|
readonly code: 27;
|
|
157
183
|
readonly message: "auth token algorithm {alg} is not allowed";
|
|
158
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* M4 (D48): the project is at a usage cap and this join is new work.
|
|
187
|
+
*
|
|
188
|
+
* The template carries `{detail}` rather than a fixed sentence because the actionable half
|
|
189
|
+
* differs by tier and by meter, and a refusal that does not say what to do next is the thing
|
|
190
|
+
* `errors.md`'s fix-line convention exists to prevent. Control composes the detail; the box
|
|
191
|
+
* relays it. Non-protocol surfaces (the store gateway's write refusal, control's HTTP refusals
|
|
192
|
+
* for deploys and uploads) reuse the same `E_USAGE_CAP` string code in their own shapes, so one
|
|
193
|
+
* grep finds every place a cap can be met.
|
|
194
|
+
*/
|
|
195
|
+
readonly E_USAGE_CAP: {
|
|
196
|
+
readonly code: 28;
|
|
197
|
+
readonly message: "{detail}";
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* M4 part 5 (D53): the tenant cannot verify platform identity assertions because it holds no
|
|
201
|
+
* platform public key yet.
|
|
202
|
+
*
|
|
203
|
+
* Deliberately not `E_AUTH` and not `E_TOKEN_INVALID`. Those say "your credential is bad", and
|
|
204
|
+
* this is the opposite — the credential may be perfect and the box is not yet in a position to
|
|
205
|
+
* say so. Keys are delivered to a tenant the way cap state is (a push from control right after
|
|
206
|
+
* the VM comes up), so the honest reading of this code is "retry in a moment", which is what
|
|
207
|
+
* the fix line in the errors reference says.
|
|
208
|
+
*/
|
|
209
|
+
readonly E_ASSERTION_UNVERIFIABLE: {
|
|
210
|
+
readonly code: 29;
|
|
211
|
+
readonly message: "this server cannot verify identity assertions yet";
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* M5 part 3.5: a room type is already running as many rooms as it declared it would.
|
|
215
|
+
*
|
|
216
|
+
* Deliberately its own code rather than `E_ROOM_FULL`, which is about clients in a room, and
|
|
217
|
+
* emphatically not `E_INTERNAL`. This refusal is what makes declared sizing honest: the tenant's
|
|
218
|
+
* VM memory was computed as the declared per-room heap times this number, so room N+1 is a room
|
|
219
|
+
* the machine was never built to hold. The message names the type, the limit and the field to
|
|
220
|
+
* change, because all three are things the developer controls.
|
|
221
|
+
*/
|
|
222
|
+
readonly E_TYPE_AT_CAPACITY: {
|
|
223
|
+
readonly code: 30;
|
|
224
|
+
readonly message: "{detail}";
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* M5 part 3.5 (Part C): the project has used its egress allowance and forwarding has stopped.
|
|
228
|
+
*
|
|
229
|
+
* Its own code, and its own WebSocket close code ({@link CLOSE_EGRESS_WALL}), for one reason: a
|
|
230
|
+
* developer watching sockets close has to be able to tell a wall from a bug. `E_USAGE_CAP` is
|
|
231
|
+
* the polite refusal of *new* work at a cap control evaluated up to a minute ago; this is the
|
|
232
|
+
* box stopping traffic on its own, locally, the moment its leased budget ran out.
|
|
233
|
+
*/
|
|
234
|
+
readonly E_EGRESS_WALL: {
|
|
235
|
+
readonly code: 31;
|
|
236
|
+
readonly message: "{detail}";
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* M5 part 3.5 (Part C): a free-tier shape limit — the relay client cap, or the concurrent
|
|
240
|
+
* awake-rooms cap. The message names the limit and the tier, because the fix is a card.
|
|
241
|
+
*/
|
|
242
|
+
readonly E_TIER_LIMIT: {
|
|
243
|
+
readonly code: 32;
|
|
244
|
+
readonly message: "{detail}";
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* M5 part 7 (D67-i): the project's region has no server with room for another tenant VM.
|
|
248
|
+
*
|
|
249
|
+
* Sent by the router, fatal, with close code 1013 (RFC 6455 "try again later"). Before this
|
|
250
|
+
* code a full region looked like a slow start: the control plane re-ran placement on every
|
|
251
|
+
* lookup poll for thirty seconds and the client then saw `E_STARTING` followed by `E_INTERNAL`.
|
|
252
|
+
* The message names the region and the two fixes because both are an operator's, never the
|
|
253
|
+
* player's: the client does not retry this join on its own.
|
|
254
|
+
*/
|
|
255
|
+
readonly E_PLACEMENT: {
|
|
256
|
+
readonly code: 33;
|
|
257
|
+
readonly message: "no capacity in region {region}; add a server or raise maxVms";
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* M6 lane A (D68-d): an operator deleted this room, and its stored state is gone.
|
|
261
|
+
*
|
|
262
|
+
* Its own code rather than `E_KICKED` or `E_ROOM_CLOSED`, and the difference between the three is
|
|
263
|
+
* the reason it exists. `E_KICKED` is the room's own code deciding one player should leave, and
|
|
264
|
+
* a client's right answer is usually to show a message and stay in the game. `E_ROOM_CLOSED` is
|
|
265
|
+
* a room ending normally; rejoining makes a fresh one and nothing was lost. This is neither: the
|
|
266
|
+
* room's state has been deleted from outside the game, deliberately, by somebody with a rooms
|
|
267
|
+
* API credential. A client that reconnects gets an empty room rather than the one it was in.
|
|
268
|
+
* Different thing to tell a player, different thing for a client to do, different code.
|
|
269
|
+
*
|
|
270
|
+
* Fatal, which is load-bearing: `@irtio/client`'s fatal path leaves for good rather than
|
|
271
|
+
* reconnecting, and that is what stops a deleted room from being immediately recreated by the
|
|
272
|
+
* very clients that were in it. Close code {@link CLOSE_ROOM_DELETED}.
|
|
273
|
+
*/
|
|
274
|
+
readonly E_ROOM_DELETED: {
|
|
275
|
+
readonly code: 34;
|
|
276
|
+
readonly message: "room {roomId} was deleted by an operator; its stored state is gone";
|
|
277
|
+
};
|
|
159
278
|
};
|
|
279
|
+
/**
|
|
280
|
+
* The WebSocket close code the router uses for {@link ErrorCode.E_PLACEMENT}: RFC 6455's 1013,
|
|
281
|
+
* "try again later". Distinct from the 1008 policy close and the 1011 internal-error close the
|
|
282
|
+
* router uses for everything else, so a watcher of close codes can tell a full region from a bug.
|
|
283
|
+
*/
|
|
284
|
+
declare const CLOSE_TRY_AGAIN_LATER = 1013;
|
|
285
|
+
/**
|
|
286
|
+
* The WebSocket close code for the egress wall (M5 part 3.5, Part C).
|
|
287
|
+
*
|
|
288
|
+
* In the application range (4000-4999) and distinct from the `1008` policy close, which is the
|
|
289
|
+
* whole point: when the wall trips, forwarding stops and sockets close, and a developer has to be
|
|
290
|
+
* able to tell that apart from a crash, a slow consumer or an origin refusal without reading a log
|
|
291
|
+
* they may not have. 4290 echoes HTTP 429 for the same reason a person would guess it does.
|
|
292
|
+
*/
|
|
293
|
+
declare const CLOSE_EGRESS_WALL = 4290;
|
|
294
|
+
/**
|
|
295
|
+
* The WebSocket close code for {@link ErrorCode.E_ROOM_DELETED} (D68-d).
|
|
296
|
+
*
|
|
297
|
+
* In the application range beside {@link CLOSE_EGRESS_WALL}, and its own value for the same
|
|
298
|
+
* reason: a developer watching sockets close has to be able to tell an operator deleting a room
|
|
299
|
+
* from a wall, from a policy refusal, and from a crash, without a log they may not have. 4291 sits
|
|
300
|
+
* next to 4290 because both are "the platform stopped this on purpose", and the two are one apart
|
|
301
|
+
* so a grep for `429` in a close-code histogram finds both.
|
|
302
|
+
*/
|
|
303
|
+
declare const CLOSE_ROOM_DELETED = 4291;
|
|
160
304
|
type ErrorCodeName = keyof typeof ErrorCode;
|
|
161
305
|
interface ErrorCatalogueEntry {
|
|
162
306
|
readonly name: ErrorCodeName;
|
|
@@ -192,6 +336,24 @@ type Credential = {
|
|
|
192
336
|
readonly kind: 'jwt';
|
|
193
337
|
readonly key: string;
|
|
194
338
|
readonly token: string;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* D53: the project key AND a platform identity **assertion** — audience-bound to this project,
|
|
342
|
+
* expiring in minutes, carrying a per-project pseudonymous subject, and signed asymmetrically
|
|
343
|
+
* so the guest can verify it but not mint it. Wire tag 3.
|
|
344
|
+
*
|
|
345
|
+
* A separate tag rather than a second flavour of `jwt` on purpose. The two are verified by
|
|
346
|
+
* different code with different key material and different trust: a BYO JWT is signed by the
|
|
347
|
+
* tenant's own secret (which the guest also holds), an assertion is signed by the platform
|
|
348
|
+
* (which the guest holds only the public half of). Sharing a tag would mean one verifier
|
|
349
|
+
* deciding between them from a header claim, which is the algorithm-confusion hole with extra
|
|
350
|
+
* steps. The player's actual identity credential is never on this wire at all — it is
|
|
351
|
+
* exchanged with control over HTTPS before the socket opens.
|
|
352
|
+
*/
|
|
353
|
+
| {
|
|
354
|
+
readonly kind: 'assertion';
|
|
355
|
+
readonly key: string;
|
|
356
|
+
readonly token: string;
|
|
195
357
|
};
|
|
196
358
|
interface Hello {
|
|
197
359
|
readonly protocolVersion: number;
|
|
@@ -205,7 +367,23 @@ interface Hello {
|
|
|
205
367
|
readonly role?: string | undefined;
|
|
206
368
|
/** Display name requested at join. */
|
|
207
369
|
readonly name?: string | undefined;
|
|
370
|
+
/**
|
|
371
|
+
* D50: this client can rebuild its codec from a `SCHEMA` frame, so an additive deploy may swap
|
|
372
|
+
* its schema in place instead of closing the socket with `E_SCHEMA_MISMATCH`. Carried as a bit
|
|
373
|
+
* in the existing optional-presence mask rather than as an appended field, because `decodeHello`
|
|
374
|
+
* ends in `assertEof` and every deployed supervisor would reject trailing bytes.
|
|
375
|
+
*
|
|
376
|
+
* A capability, not a request: the server still decides, and still drains on a breaking change.
|
|
377
|
+
* Absent (the default) means today's behaviour exactly, and means the server must never send
|
|
378
|
+
* this session frame 13 — `decodeFrame` throws on an unknown type.
|
|
379
|
+
*/
|
|
380
|
+
readonly schemaSwap?: boolean | undefined;
|
|
208
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* D50 capability bit. Bits 0-2 each announce an optional *value* that follows the mask; this one
|
|
384
|
+
* carries no payload, it is a pure flag, so it adds nothing to the encoded length. Bits 4-7 free.
|
|
385
|
+
*/
|
|
386
|
+
declare const HELLO_SCHEMA_SWAP_BIT: number;
|
|
209
387
|
declare function encodeHello(h: Hello): Uint8Array;
|
|
210
388
|
declare function decodeHello(bytes: Uint8Array): Hello;
|
|
211
389
|
interface Welcome {
|
|
@@ -223,12 +401,24 @@ interface Welcome {
|
|
|
223
401
|
*/
|
|
224
402
|
readonly roomId: string;
|
|
225
403
|
/**
|
|
226
|
-
* The room's tick
|
|
227
|
-
*
|
|
228
|
-
* defaults to `max(50,
|
|
229
|
-
*
|
|
404
|
+
* The room's tick rate in ticks per second, or `0` when the sender does not know it (a relay
|
|
405
|
+
* room). The client derives everything it needs in milliseconds from this: the interpolation
|
|
406
|
+
* delay defaults to `max(50, 2000 / tickRate)` (D20), and a predicted physics world steps at
|
|
407
|
+
* `1 / tickRate` seconds.
|
|
408
|
+
*
|
|
409
|
+
* This slot carried `tickIntervalMs`, `round(1000 / tickRate)`, until `PROTOCOL_VERSION` 2.
|
|
410
|
+
* Rounding to whole milliseconds is lossy at exactly the rates games use: 60 Hz arrived as 17
|
|
411
|
+
* and 30 Hz as 33, so a client that inherited its timestep stepped its local world 2% slower
|
|
412
|
+
* than the server stepped its own, and paid for the drift in corrections. The rate is the
|
|
413
|
+
* number the room actually configured, and every derived millisecond figure is now exact.
|
|
230
414
|
*/
|
|
231
|
-
readonly
|
|
415
|
+
readonly tickRate: number;
|
|
416
|
+
/**
|
|
417
|
+
* The room's configured client cap, or `0` when the sender does not know it (a relay room,
|
|
418
|
+
* or a server predating this field). Appended after `tickRate`, same additive style as
|
|
419
|
+
* `tickRate` itself.
|
|
420
|
+
*/
|
|
421
|
+
readonly maxClients: number;
|
|
232
422
|
}
|
|
233
423
|
declare function encodeWelcome(w0: Welcome): Uint8Array;
|
|
234
424
|
declare function decodeWelcome(bytes: Uint8Array): Welcome;
|
|
@@ -282,10 +472,35 @@ declare function readCorrectAppliedTick(r: ByteReader): number | undefined;
|
|
|
282
472
|
declare function encodeDeltaFrame(codecBytes: Uint8Array): Uint8Array;
|
|
283
473
|
declare function encodeWriteFrame(codecBytes: Uint8Array): Uint8Array;
|
|
284
474
|
declare function encodeCorrectFrame(codecBytes: Uint8Array, clientTick: number, appliedTick?: number): Uint8Array;
|
|
475
|
+
/**
|
|
476
|
+
* The `SCHEMA` frame payload: the new schema's canonical JSON as UTF-8, exactly the string
|
|
477
|
+
* `Schema.canonical` produces and `Deployment.schemaJson` already stores. No length prefix and no
|
|
478
|
+
* envelope — the frame body *is* the descriptor, so `encodeSchemaFrame`/`readSchemaPayload` are a
|
|
479
|
+
* `TextEncoder`/`TextDecoder` pair and nothing more.
|
|
480
|
+
*
|
|
481
|
+
* There is deliberately no new codec here. The largest schema in the repo is 4.3 KB raw and 557
|
|
482
|
+
* bytes gzipped (M4 part 1 report §8.1), which is not worth a wire format; and reusing the exact
|
|
483
|
+
* bytes the control plane already stores means the descriptor a client rebuilds from is provably
|
|
484
|
+
* the one that was deployed, not a re-serialisation of it.
|
|
485
|
+
*/
|
|
486
|
+
declare function schemaPayload(canonical: string): Uint8Array;
|
|
487
|
+
declare function readSchemaPayload(payload: Uint8Array): string;
|
|
488
|
+
declare function encodeSchemaFrame(canonical: string): Uint8Array;
|
|
285
489
|
|
|
286
490
|
interface Call {
|
|
287
491
|
readonly reqId: number;
|
|
288
492
|
readonly rpcId: number;
|
|
493
|
+
/**
|
|
494
|
+
* D72: the newest authoritative tick the caller had applied when it sent this `CALL`, so a room
|
|
495
|
+
* can answer the shot against the world the shooter was looking at (`ctx.clientTick`,
|
|
496
|
+
* `room.rewind`). `0` means "no stamp" — a server-to-client `CALL`, or a client that does not
|
|
497
|
+
* send one — and ticks start at 1, so zero can never be a real tick.
|
|
498
|
+
*
|
|
499
|
+
* It is a client-supplied number that selects which past the server answers from, and a client
|
|
500
|
+
* can lie about it. What a lie can reach is bounded by the room's history depth, and the room
|
|
501
|
+
* decides what it does with the answer; `concepts/lag-compensation.md` says both.
|
|
502
|
+
*/
|
|
503
|
+
readonly clientTick?: number;
|
|
289
504
|
/** Rest-of-buffer: opaque schema-codec-encoded params (not length-prefixed). */
|
|
290
505
|
readonly params: Uint8Array;
|
|
291
506
|
}
|
|
@@ -328,6 +543,146 @@ declare function rpcByIdOf(schema: AnySchema, id: number): RpcDesc;
|
|
|
328
543
|
*/
|
|
329
544
|
type ClientCallable<R extends RpcMap> = ServerRpcs<R> & typeof builtinRpcs;
|
|
330
545
|
|
|
546
|
+
/**
|
|
547
|
+
* The bandwidth ledger (D65): cumulative bytes per direction, per row, for one room or one
|
|
548
|
+
* client session.
|
|
549
|
+
*
|
|
550
|
+
* The row set below is the product contract — the docs list it, the overlay renders it, and the
|
|
551
|
+
* CLI prints it — so it is a small closed vocabulary rather than free-form tags. Rates are never
|
|
552
|
+
* stored here: a reader diffs two snapshots over a window and divides. Cumulative counters
|
|
553
|
+
* survive a missed poll; a stored rate does not.
|
|
554
|
+
*
|
|
555
|
+
* ## Conservation
|
|
556
|
+
*
|
|
557
|
+
* For every frame handed to `attribute`, the rows it adds sum to exactly the frame's length,
|
|
558
|
+
* including the envelope byte. That is structural, not aspirational: the attribution is staged
|
|
559
|
+
* into a scratch list, and whatever the walker could not account for (a delta encoded against
|
|
560
|
+
* another schema, a truncated frame, a frame type from a newer peer) is committed to
|
|
561
|
+
* `overhead/unattributed` rather than dropped. A profiler that quietly loses bytes would be
|
|
562
|
+
* worse than no profiler, because its shares would look right.
|
|
563
|
+
*/
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* The closed kind vocabulary. `field` is the steady state; every other kind exists because
|
|
567
|
+
* lumping it into `field` would hide the thing a reader came to find.
|
|
568
|
+
*/
|
|
569
|
+
type ProfileKind = 'field' | 'presence' | 'correction' | 'write' | 'churn' | 'rpc' | 'message' | 'voice' | 'join' | 'overhead' | 'control';
|
|
570
|
+
declare const PROFILE_KINDS: readonly ProfileKind[];
|
|
571
|
+
interface ProfileRow {
|
|
572
|
+
readonly kind: ProfileKind;
|
|
573
|
+
/** `collection.field`, an rpc name, or one of the fixed keys the kind table documents. */
|
|
574
|
+
readonly key: string;
|
|
575
|
+
/** Cumulative bytes sent to the peer. */
|
|
576
|
+
readonly out: number;
|
|
577
|
+
/** Cumulative bytes received from the peer. */
|
|
578
|
+
readonly in: number;
|
|
579
|
+
}
|
|
580
|
+
/** A ledger, flattened for transport (the worker `stats` reply, `state.json`, the overlay). */
|
|
581
|
+
interface ProfileSnapshot {
|
|
582
|
+
readonly rows: readonly ProfileRow[];
|
|
583
|
+
readonly bytesIn: number;
|
|
584
|
+
readonly bytesOut: number;
|
|
585
|
+
readonly framesIn: number;
|
|
586
|
+
readonly framesOut: number;
|
|
587
|
+
/**
|
|
588
|
+
* How many frames the walker actually read. Lower than `framesOut` exactly when a payload was
|
|
589
|
+
* shared by several recipients: the bytes count once per recipient (that is what left the box)
|
|
590
|
+
* but the walk happens once per encode. The adversarial tests read this.
|
|
591
|
+
*/
|
|
592
|
+
readonly walks: number;
|
|
593
|
+
}
|
|
594
|
+
declare const EMPTY_PROFILE: ProfileSnapshot;
|
|
595
|
+
/** AOI enter/leave ids for one client's frame, by collection. */
|
|
596
|
+
type ChurnIds = ReadonlyMap<string, ReadonlySet<string>>;
|
|
597
|
+
interface AttributeOptions {
|
|
598
|
+
/**
|
|
599
|
+
* Ids whose whole `add`/`remove` op is visibility churn rather than a real spawn or despawn.
|
|
600
|
+
* Server-side only: only the encoder knows which ops the global dirty set never contained.
|
|
601
|
+
*/
|
|
602
|
+
readonly churn?: ChurnIds | undefined;
|
|
603
|
+
/**
|
|
604
|
+
* The shared payload object this frame was built from. Frames built from the same payload in
|
|
605
|
+
* one flush are walked once and replayed per recipient. Cleared by `newFlush()`.
|
|
606
|
+
*/
|
|
607
|
+
readonly shared?: object | undefined;
|
|
608
|
+
/** Correlation scope for matching a REPLY back to its CALL's name. A client id, usually. */
|
|
609
|
+
readonly peer?: string | undefined;
|
|
610
|
+
/**
|
|
611
|
+
* The client's approximation of churn: every `add`/`remove` in a `spatial-grid` collection is
|
|
612
|
+
* counted as visibility churn, because a decoder genuinely cannot tell a synthetic add from a
|
|
613
|
+
* real spawn — they are the same bytes. Surfaces that use it must say so; the overlay labels
|
|
614
|
+
* the row `enter/leave (incl. spawns)`.
|
|
615
|
+
*/
|
|
616
|
+
readonly spatialChurn?: boolean | undefined;
|
|
617
|
+
}
|
|
618
|
+
declare class ProfileLedger {
|
|
619
|
+
private readonly rows;
|
|
620
|
+
private readonly memo;
|
|
621
|
+
/** Peer and reqId to the rpc name its CALL carried, so the matching REPLY can be named too. */
|
|
622
|
+
private readonly pending;
|
|
623
|
+
private scratch;
|
|
624
|
+
private bytesIn;
|
|
625
|
+
private bytesOut;
|
|
626
|
+
private framesIn;
|
|
627
|
+
private framesOut;
|
|
628
|
+
private walkCount;
|
|
629
|
+
private rpcSchema;
|
|
630
|
+
/**
|
|
631
|
+
* `schema` is the runtime-extended schema (`withBuiltins`), which is what delta and snapshot
|
|
632
|
+
* bodies are encoded against. `rpcSchema` is the one whose `rpcTable` the `rpcId` on the wire
|
|
633
|
+
* indexes; the two tables agree today (extending adds a collection, not an rpc), but the
|
|
634
|
+
* senders name them separately and so does this.
|
|
635
|
+
*/
|
|
636
|
+
private schema;
|
|
637
|
+
constructor(schema: AnySchema, rpcSchema?: AnySchema);
|
|
638
|
+
/**
|
|
639
|
+
* D50: a session whose schema was swapped mid-flight keeps its ledger and its accumulated rows.
|
|
640
|
+
* Rows named after a field the new schema dropped simply stop growing, which is the honest
|
|
641
|
+
* reading — those bytes really were spent.
|
|
642
|
+
*/
|
|
643
|
+
swapSchema(schema: AnySchema, rpcSchema?: AnySchema): void;
|
|
644
|
+
get walks(): number;
|
|
645
|
+
/** Drops the shared-payload memo. Call at the top of each flush. */
|
|
646
|
+
newFlush(): void;
|
|
647
|
+
reset(): void;
|
|
648
|
+
snapshot(): ProfileSnapshot;
|
|
649
|
+
/**
|
|
650
|
+
* Attributes one whole frame, envelope byte included. Never throws: a frame this ledger cannot
|
|
651
|
+
* read still contributes its full length, to `overhead/unattributed`.
|
|
652
|
+
*/
|
|
653
|
+
attribute(dir: 'in' | 'out', frame: Uint8Array, options?: AttributeOptions): void;
|
|
654
|
+
/**
|
|
655
|
+
* Attributes bare snapshot bytes under `join`. The room worker needs this: it hands the host
|
|
656
|
+
* snapshot bytes and the host wraps them in a `WELCOME`, so the room never sees that frame,
|
|
657
|
+
* but the bytes are still the room's egress and the join rows are the reason anyone profiles a
|
|
658
|
+
* join at all. A client, which does see the whole frame, uses `attribute` instead.
|
|
659
|
+
*/
|
|
660
|
+
attributeSnapshot(dir: 'in' | 'out', bytes: Uint8Array): void;
|
|
661
|
+
private commit;
|
|
662
|
+
private describe;
|
|
663
|
+
/** WELCOME: the snapshot inside it is `join`; everything around it is `control/welcome`. */
|
|
664
|
+
private welcome;
|
|
665
|
+
private snapshotBody;
|
|
666
|
+
private body;
|
|
667
|
+
/**
|
|
668
|
+
* The walker's consumer. `op` latches: a churn op routes its own framing *and* every field
|
|
669
|
+
* that follows it into `churn`, because a synthetic add carries a whole record whose bytes are
|
|
670
|
+
* the cost of the entity crossing the boundary, not the cost of the fields changing.
|
|
671
|
+
*/
|
|
672
|
+
private collector;
|
|
673
|
+
private rpcName;
|
|
674
|
+
private remember;
|
|
675
|
+
private recall;
|
|
676
|
+
}
|
|
677
|
+
/** `next` minus `prev`, row by row. Rows that did not move are dropped. */
|
|
678
|
+
declare function diffProfiles(prev: ProfileSnapshot, next: ProfileSnapshot): ProfileSnapshot;
|
|
679
|
+
/** The `n` heaviest rows, by out + in. */
|
|
680
|
+
declare function topProfileRows(snap: ProfileSnapshot, n: number): readonly ProfileRow[];
|
|
681
|
+
/** Divides every counter by `n`, for the per-bot convention `simulate` already prints. */
|
|
682
|
+
declare function scaleProfile(snap: ProfileSnapshot, n: number): ProfileSnapshot;
|
|
683
|
+
/** Adds `b` into `a`, row by row. Used to roll several sessions into one table. */
|
|
684
|
+
declare function mergeProfiles(a: ProfileSnapshot, b: ProfileSnapshot): ProfileSnapshot;
|
|
685
|
+
|
|
331
686
|
/**
|
|
332
687
|
* MSG frame payload: an out-of-band message with an addressing discriminator. The same
|
|
333
688
|
* shape is reused in both directions — client→server as the send *target*, server→client as
|
|
@@ -343,15 +698,177 @@ type MsgTarget = {
|
|
|
343
698
|
readonly role: string;
|
|
344
699
|
} | {
|
|
345
700
|
readonly kind: 'server';
|
|
701
|
+
} | {
|
|
702
|
+
readonly kind: 'voice';
|
|
346
703
|
};
|
|
347
704
|
interface Msg {
|
|
348
705
|
readonly target: MsgTarget;
|
|
349
706
|
/** Rest-of-buffer: opaque application payload. */
|
|
350
707
|
readonly payload: Uint8Array;
|
|
708
|
+
/**
|
|
709
|
+
* D70: present when this is a **typed** message — one of the shapes the schema declares, whose
|
|
710
|
+
* `payload` is `encodeFields(schema.messages[index].fields, value)` rather than opaque bytes.
|
|
711
|
+
*
|
|
712
|
+
* Absent for a raw `room.message(target, bytes)`. The two never mix: a receiver hands a typed
|
|
713
|
+
* frame only to typed listeners and a raw frame only to raw ones, on the client and in a room
|
|
714
|
+
* alike, so game code is never handed one under the other's API.
|
|
715
|
+
*/
|
|
716
|
+
readonly typed?: {
|
|
717
|
+
readonly index: number;
|
|
718
|
+
};
|
|
351
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* D70 typed peer messages: an **envelope**, not a sixth address.
|
|
722
|
+
*
|
|
723
|
+
* The addressing byte is the one byte the format already spends on every MSG, so a typed message
|
|
724
|
+
* costs a discriminator it was going to pay for plus two bytes of index — no new `FrameType`, and
|
|
725
|
+
* directed delivery keeps working because the envelope carries a target of its own right after
|
|
726
|
+
* the index. Layout:
|
|
727
|
+
*
|
|
728
|
+
* [5][index u16 LE][inner target kind][inner target fields][encoded payload]
|
|
729
|
+
*
|
|
730
|
+
* The inner target is the same vocabulary as an untyped MSG minus voice: `all`/`client`/`role`
|
|
731
|
+
* inbound, and the sender's `client` or `server` on the way out (the fan-out rewrites the slot,
|
|
732
|
+
* exactly as it does for raw bytes). `voice` is refused on both encode and decode — a typed
|
|
733
|
+
* envelope is authored by game code, and voice signaling must stay unrepresentable there.
|
|
734
|
+
*
|
|
735
|
+
* `isVoiceMsg` is unaffected: a typed frame's first byte is 5, never 4.
|
|
736
|
+
*/
|
|
737
|
+
declare const MSG_KIND_TYPED = 5;
|
|
738
|
+
/**
|
|
739
|
+
* A one-byte peek: is this MSG payload voice signaling? The supervisor calls this on every MSG
|
|
740
|
+
* before deciding where the frame goes, so it must not allocate or decode. An empty payload is
|
|
741
|
+
* not voice (it is malformed, and the existing paths report that).
|
|
742
|
+
*/
|
|
743
|
+
declare function isVoiceMsg(payload: Uint8Array): boolean;
|
|
744
|
+
/**
|
|
745
|
+
* D70: the same one-byte peek for a typed envelope. The supervisor uses it on the hot frame path
|
|
746
|
+
* for the same reason it uses `isVoiceMsg` — to decide where a frame goes without decoding it.
|
|
747
|
+
*/
|
|
748
|
+
declare function isTypedMsg(payload: Uint8Array): boolean;
|
|
749
|
+
/**
|
|
750
|
+
* D70: may this typed envelope be **accepted from a client**?
|
|
751
|
+
*
|
|
752
|
+
* A three-byte peek, no allocation: an inbound typed message may address `all`, a client or a
|
|
753
|
+
* role, and nothing else. `server` has no meaning on the peer path (a room observes through
|
|
754
|
+
* `onMessage`; there is no typed server handler, by design) and `voice` must never be authorable
|
|
755
|
+
* by game code. Both are dropped by whoever sees the frame first, which is the supervisor above
|
|
756
|
+
* the relay fork and the relay host in the shared process.
|
|
757
|
+
*
|
|
758
|
+
* Returns `false` for anything too short to read, so a truncated frame is refused rather than
|
|
759
|
+
* assumed.
|
|
760
|
+
*/
|
|
761
|
+
declare function typedMsgFromClientOk(payload: Uint8Array): boolean;
|
|
352
762
|
declare function encodeMsg(m: Msg): Uint8Array;
|
|
353
763
|
declare function decodeMsg(bytes: Uint8Array): Msg;
|
|
354
764
|
|
|
765
|
+
/**
|
|
766
|
+
* D51 voice signaling — the message set that rides inside a `{ kind: 'voice' }` MSG.
|
|
767
|
+
*
|
|
768
|
+
* Two deliberate choices worth stating, because both are cheap to reverse later and expensive to
|
|
769
|
+
* get wrong now.
|
|
770
|
+
*
|
|
771
|
+
* **JSON, not the binary schema codec.** Every other payload on this socket is binary because it
|
|
772
|
+
* is on the tick path and its shape is fixed. Signaling is neither: a handful of messages per
|
|
773
|
+
* participant per call, carrying mediasoup's own RTP capability and DTLS parameter objects, whose
|
|
774
|
+
* shape is defined by mediasoup rather than by us. Re-encoding those into a hand-rolled binary
|
|
775
|
+
* format would buy nothing and would have to be revised every time mediasoup adds a field. The
|
|
776
|
+
* volume is a few kilobytes per join, once.
|
|
777
|
+
*
|
|
778
|
+
* **Everything here is per-project at most.** The blast radius note the docs repeat: transport
|
|
779
|
+
* parameters, ICE candidates and DTLS fingerprints are secrets about one call in one project's
|
|
780
|
+
* room. A tenant is assumed hostile and a hostile tenant can already do anything it likes to its
|
|
781
|
+
* own game — the same stance as BYO JWT signing keys. No platform credential, no agent token and
|
|
782
|
+
* no other project's identifier is representable in any message below, and the SFU never holds a
|
|
783
|
+
* credential of any kind for anything to leak (fact 5, and bug #27's lesson).
|
|
784
|
+
*/
|
|
785
|
+
/** Client → SFU. */
|
|
786
|
+
type VoiceClientMessage = {
|
|
787
|
+
readonly t: 'join';
|
|
788
|
+
readonly rtpCapabilities: unknown;
|
|
789
|
+
} | {
|
|
790
|
+
readonly t: 'connect';
|
|
791
|
+
readonly transportId: string;
|
|
792
|
+
readonly dtlsParameters: unknown;
|
|
793
|
+
} | {
|
|
794
|
+
readonly t: 'produce';
|
|
795
|
+
readonly transportId: string;
|
|
796
|
+
readonly kind: 'audio';
|
|
797
|
+
readonly rtpParameters: unknown;
|
|
798
|
+
} | {
|
|
799
|
+
readonly t: 'consume';
|
|
800
|
+
readonly producerId: string;
|
|
801
|
+
readonly rtpCapabilities: unknown;
|
|
802
|
+
} | {
|
|
803
|
+
readonly t: 'resume';
|
|
804
|
+
readonly consumerId: string;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Mute is signalled, not merely stopped locally, because the meter depends on it: an SFU that
|
|
808
|
+
* only sees a silent stream cannot tell a muted participant from a quiet room, and §1.3 makes
|
|
809
|
+
* mute the boundary between minutes (which keep accruing) and egress (which stops).
|
|
810
|
+
*/
|
|
811
|
+
| {
|
|
812
|
+
readonly t: 'mute';
|
|
813
|
+
readonly muted: boolean;
|
|
814
|
+
} | {
|
|
815
|
+
readonly t: 'leave';
|
|
816
|
+
};
|
|
817
|
+
/** SFU → client. */
|
|
818
|
+
type VoiceServerMessage = {
|
|
819
|
+
readonly t: 'joined';
|
|
820
|
+
readonly sendTransport: VoiceTransportInfo;
|
|
821
|
+
readonly recvTransport: VoiceTransportInfo;
|
|
822
|
+
readonly routerRtpCapabilities: unknown;
|
|
823
|
+
readonly peers: readonly VoicePeer[];
|
|
824
|
+
} | {
|
|
825
|
+
readonly t: 'produced';
|
|
826
|
+
readonly producerId: string;
|
|
827
|
+
} | {
|
|
828
|
+
readonly t: 'consumed';
|
|
829
|
+
readonly consumerId: string;
|
|
830
|
+
readonly producerId: string;
|
|
831
|
+
readonly peerId: string;
|
|
832
|
+
readonly kind: 'audio';
|
|
833
|
+
readonly rtpParameters: unknown;
|
|
834
|
+
} | {
|
|
835
|
+
readonly t: 'connected';
|
|
836
|
+
readonly transportId: string;
|
|
837
|
+
} | {
|
|
838
|
+
readonly t: 'peer-joined';
|
|
839
|
+
readonly peer: VoicePeer;
|
|
840
|
+
} | {
|
|
841
|
+
readonly t: 'peer-left';
|
|
842
|
+
readonly peerId: string;
|
|
843
|
+
} | {
|
|
844
|
+
readonly t: 'peer-muted';
|
|
845
|
+
readonly peerId: string;
|
|
846
|
+
readonly muted: boolean;
|
|
847
|
+
} | {
|
|
848
|
+
readonly t: 'error';
|
|
849
|
+
readonly code: string;
|
|
850
|
+
readonly message: string;
|
|
851
|
+
};
|
|
852
|
+
interface VoiceTransportInfo {
|
|
853
|
+
readonly id: string;
|
|
854
|
+
readonly iceParameters: unknown;
|
|
855
|
+
readonly iceCandidates: unknown;
|
|
856
|
+
readonly dtlsParameters: unknown;
|
|
857
|
+
}
|
|
858
|
+
interface VoicePeer {
|
|
859
|
+
/** The room's client id — the same identifier the render store keys entities' owners by. */
|
|
860
|
+
readonly peerId: string;
|
|
861
|
+
readonly producerId?: string;
|
|
862
|
+
readonly muted: boolean;
|
|
863
|
+
}
|
|
864
|
+
declare function encodeVoiceMessage(m: VoiceClientMessage | VoiceServerMessage): Uint8Array;
|
|
865
|
+
/**
|
|
866
|
+
* Returns `undefined` rather than throwing on anything malformed. This is parsing bytes that
|
|
867
|
+
* arrived over a socket from an untrusted peer; the callers all want "ignore it", and a throw on
|
|
868
|
+
* the supervisor's frame path would be a denial of service with extra steps.
|
|
869
|
+
*/
|
|
870
|
+
declare function decodeVoiceMessage<T extends VoiceClientMessage | VoiceServerMessage>(bytes: Uint8Array): T | undefined;
|
|
871
|
+
|
|
355
872
|
declare const presenceEntity: _irtio_schema.EntityDef<{
|
|
356
873
|
clientId: _irtio_schema.Type<string, false>;
|
|
357
874
|
role: _irtio_schema.Type<string, false>;
|
|
@@ -370,7 +887,7 @@ type PresenceRecord = InstanceOf<typeof presenceEntity>;
|
|
|
370
887
|
*/
|
|
371
888
|
declare function withBuiltins<S extends AnySchema>(schema: S): Schema<SchemaDefs<S> & {
|
|
372
889
|
clients: typeof presenceEntity;
|
|
373
|
-
}, SchemaRpc<S>, SchemaRoles<S>>;
|
|
890
|
+
}, SchemaRpc<S>, SchemaRoles<S>, SchemaMessages<S>>;
|
|
374
891
|
/**
|
|
375
892
|
* The schema of a room with nothing deployed — no builder collections, just the
|
|
376
893
|
* built-in presence merged by `withBuiltins`. The supervisor's relay forwarder encodes presence
|
|
@@ -379,11 +896,673 @@ declare function withBuiltins<S extends AnySchema>(schema: S): Schema<SchemaDefs
|
|
|
379
896
|
*/
|
|
380
897
|
declare const relaySchema: Schema<{
|
|
381
898
|
clients: typeof presenceEntity;
|
|
382
|
-
}, {}, readonly string[]>;
|
|
899
|
+
}, {}, readonly string[], {}>;
|
|
383
900
|
/** The `schemaHash8` a relay HELLO carries: 8 zero bytes ("no schema"). */
|
|
384
901
|
declare const RELAY_HASH8: Uint8Array;
|
|
385
902
|
declare function isRelayHash8(hash8: Uint8Array): boolean;
|
|
386
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Room-code constants, in the one package every component that has an opinion about them already
|
|
906
|
+
* depends on.
|
|
907
|
+
*
|
|
908
|
+
* They started in `@irtio/supervisor` (`codes.ts`), which is still the only place that *allocates*
|
|
909
|
+
* a code for a running room. D52 added a second minter: quick-match answers a filled queue with a
|
|
910
|
+
* fresh code from the control plane, which has no supervisor dependency and never will (control
|
|
911
|
+
* placing a room would drag cross-room state into a deliberately single-room component). Rather
|
|
912
|
+
* than let control keep a private copy of an alphabet that is a public contract, the contract
|
|
913
|
+
* moved here and `supervisor/codes.ts` re-exports it — the same move `auth.ts` already made for
|
|
914
|
+
* the origin policy when the router became a second enforcer of it.
|
|
915
|
+
*/
|
|
916
|
+
/** The unambiguous base-32 alphabet room codes are drawn from: no `0`/`O`, no `1`/`I`. */
|
|
917
|
+
declare const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
918
|
+
/**
|
|
919
|
+
* The one character that separates a room type from a room id, decided once (M5 part 2, shard
|
|
920
|
+
* note 1).
|
|
921
|
+
*
|
|
922
|
+
* Room types and tenant sharding both want to read structure out of a roomId, and if they reach
|
|
923
|
+
* for their own separators independently they collide. So the delimiter is a contract, stated
|
|
924
|
+
* here, and `parseRoomId` below is the only code that knows it. Two rules follow, binding on
|
|
925
|
+
* everything downstream:
|
|
926
|
+
*
|
|
927
|
+
* - It is never `/`. The alarm sidecar key is `${liveKey}/alarms` and its reverse parse assumes
|
|
928
|
+
* the room segment holds no `/`.
|
|
929
|
+
* - Anything that special-cases a particular room (a directory-pinned `notifications:global`,
|
|
930
|
+
* later) matches the whole roomId by value. It never splits on this character to do it.
|
|
931
|
+
*/
|
|
932
|
+
declare const ROOM_TYPE_DELIMITER = ":";
|
|
933
|
+
/**
|
|
934
|
+
* A room type name: lowercase, starts alphanumeric, at most 24 characters.
|
|
935
|
+
*
|
|
936
|
+
* Narrower than the id charset on purpose. A type name is a filename in `irtio/rooms/`, so it has
|
|
937
|
+
* to survive a case-insensitive filesystem, and it must never be mistaken for a room code
|
|
938
|
+
* (`looksLikeRoomCode` upper-cases 4-5 character alphanumeric ids, and lowercase-only types plus
|
|
939
|
+
* the split-before-check rule in `normalizeRoomId` keep those two worlds apart).
|
|
940
|
+
*/
|
|
941
|
+
declare const ROOM_TYPE_RE: RegExp;
|
|
942
|
+
/**
|
|
943
|
+
* Externally supplied room ids must match this (`E_ROOM_NOT_FOUND` otherwise).
|
|
944
|
+
*
|
|
945
|
+
* M5 part 2 widened it by exactly one optional prefix: `<type>:`. A plain id means the project's
|
|
946
|
+
* default room type and is accepted verbatim, exactly as it was before, which is what makes every
|
|
947
|
+
* roomId minted before this change keep meaning what it meant. The id half still carries its own
|
|
948
|
+
* 1-32 budget, so a type prefix costs a project nothing from the id it was already using.
|
|
949
|
+
*/
|
|
950
|
+
declare const ROOM_ID_RE: RegExp;
|
|
951
|
+
/**
|
|
952
|
+
* The name the default room type is registered under.
|
|
953
|
+
*
|
|
954
|
+
* A plain roomId carries no type and means this one. It is a real name rather than a `null` key so
|
|
955
|
+
* that a project can also address it explicitly (`default:lobby` and `lobby` are the same room),
|
|
956
|
+
* and so that the supervisor's type map has no special case in it.
|
|
957
|
+
*/
|
|
958
|
+
declare const DEFAULT_ROOM_TYPE = "default";
|
|
959
|
+
/** A roomId split into its optional type and its id half. */
|
|
960
|
+
interface ParsedRoomId {
|
|
961
|
+
/** The declared room type, or `undefined` for a plain id, which means the default type. */
|
|
962
|
+
readonly type: string | undefined;
|
|
963
|
+
/** The id half: what a room code, a match code, or `__tenant` would be. */
|
|
964
|
+
readonly id: string;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Splits `<type>:<id>` into its halves, or returns `undefined` when the string is not a legal
|
|
968
|
+
* roomId at all. The sole reader of `ROOM_TYPE_DELIMITER`.
|
|
969
|
+
*
|
|
970
|
+
* A plain id parses to `{ type: undefined, id: raw }` rather than to a default type name, because
|
|
971
|
+
* the default type's *name* is a per-project fact the protocol package does not know.
|
|
972
|
+
*/
|
|
973
|
+
declare function parseRoomId(raw: string): ParsedRoomId | undefined;
|
|
974
|
+
/** Joins a type and an id back into a roomId. A missing type gives the plain form. */
|
|
975
|
+
declare function formatRoomId(type: string | undefined, id: string): string;
|
|
976
|
+
/**
|
|
977
|
+
* How many characters a control-minted quick-match code carries.
|
|
978
|
+
*
|
|
979
|
+
* Ten, not four. The supervisor's own codes start at four because they are read aloud and typed
|
|
980
|
+
* in by a human, and it grows them on collision because it can see every room it holds. Control
|
|
981
|
+
* can see no such thing: it mints without asking any box whether the code is free (fact 2 — no
|
|
982
|
+
* occupancy read, no round trip), so the code has to be long enough that a collision is not a
|
|
983
|
+
* thing that happens rather than a thing that is retried. Ten characters of this alphabet is 50
|
|
984
|
+
* bits.
|
|
985
|
+
*
|
|
986
|
+
* It is also deliberately longer than five, which is what `looksLikeRoomCode` treats as
|
|
987
|
+
* code-shaped and upper-cases: a ten-character code is passed through verbatim by
|
|
988
|
+
* `normalizeRoomId`, so the string control hands a client is exactly the string the supervisor
|
|
989
|
+
* resolves.
|
|
990
|
+
*/
|
|
991
|
+
declare const MATCH_CODE_LENGTH = 10;
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* D59: the tenant-local room bus — limits, error names, and the pure validators.
|
|
995
|
+
*
|
|
996
|
+
* This module is the shared half, and it lives in `@irtio/protocol` because the two sides that
|
|
997
|
+
* must agree on it are the room runtime (which is bundled into a room worker and cannot pull in
|
|
998
|
+
* anything heavy) and the supervisor (which enforces every one of these). Neither can be trusted
|
|
999
|
+
* to check on the other's behalf: the runtime's checks are a courtesy that gives a developer a
|
|
1000
|
+
* message at the call site, and the supervisor's are the boundary, because room code runs inside
|
|
1001
|
+
* a tenant VM and anything the runtime refuses is something a hostile bundle can simply not call.
|
|
1002
|
+
*
|
|
1003
|
+
* ## The two verbs, and why their guarantees differ
|
|
1004
|
+
*
|
|
1005
|
+
* `publish`/`subscribe` is fire-and-forget fan-out to *awake* subscribers. Nothing queues, nothing
|
|
1006
|
+
* wakes, and there is no ordering promise across channels. A hibernated subscriber misses the
|
|
1007
|
+
* message the same way it misses wall-clock time, and resyncs from state when it wakes.
|
|
1008
|
+
*
|
|
1009
|
+
* `send(roomId, payload)` is directed, durable and at-least-once, and it wakes the target. It
|
|
1010
|
+
* rides the durable-alarm machinery: a mailbox entry is an alarm with a payload.
|
|
1011
|
+
*
|
|
1012
|
+
* These guarantees are deliberately weak. Sharding (M5 part 7) turns `send` into a network hop and
|
|
1013
|
+
* `publish` into a shard fan-out, and nothing stronger would survive that. **At-least-once means
|
|
1014
|
+
* at-least-once**: a handler can see the same message twice, and must be written to tolerate it.
|
|
1015
|
+
*
|
|
1016
|
+
* ## The boundary
|
|
1017
|
+
*
|
|
1018
|
+
* Tenant-scoped only, ever. There is no project argument on any verb, in any layer, at any depth.
|
|
1019
|
+
* A room can name a channel and a roomId; it cannot name a project, so a cross-project reach is
|
|
1020
|
+
* not something the supervisor refuses, it is something the API cannot express. The supervisor's
|
|
1021
|
+
* bus registry is per-tenant because a supervisor *is* one tenant, and the roomId a `send` names
|
|
1022
|
+
* is resolved against that supervisor's own room table and nothing else.
|
|
1023
|
+
*/
|
|
1024
|
+
/**
|
|
1025
|
+
* Bus limits. Every number here is enforced in the supervisor, and each is small on purpose: the
|
|
1026
|
+
* bus is signaling, not state transfer. Anything big goes through a room the recipient joins.
|
|
1027
|
+
*/
|
|
1028
|
+
declare const BUS_LIMITS: {
|
|
1029
|
+
/**
|
|
1030
|
+
* Max UTF-8 bytes in one payload, for both verbs.
|
|
1031
|
+
*
|
|
1032
|
+
* 4 KiB is a quarter of the KV value limit, and it is sized for what the feature is for: a
|
|
1033
|
+
* match-ready notice, an invite, a bracket announcement — an id, a couple of names, a reason.
|
|
1034
|
+
* A payload is also a *store* cost for `send`, because a mailbox entry persists in the alarm
|
|
1035
|
+
* sidecar, so the ceiling on one room's mailbox is this times `mailboxDepth`, and both numbers
|
|
1036
|
+
* were picked together.
|
|
1037
|
+
*/
|
|
1038
|
+
readonly payloadBytes: number;
|
|
1039
|
+
/** Max UTF-8 bytes in a channel name. Names are vocabulary, not data. */
|
|
1040
|
+
readonly channelBytes: 128;
|
|
1041
|
+
/** Max channels one room may hold a subscription to at once. */
|
|
1042
|
+
readonly subscriptionsPerRoom: 32;
|
|
1043
|
+
/**
|
|
1044
|
+
* Max undelivered mailbox entries for one target room.
|
|
1045
|
+
*
|
|
1046
|
+
* This is a store bound as much as a memory one: entries live in the alarm sidecar, so at the
|
|
1047
|
+
* payload cap above, a full mailbox is 256 KiB of persisted object. Past this, a `send` is
|
|
1048
|
+
* refused at the sender with a named error rather than silently dropped at the target, because
|
|
1049
|
+
* a sender that is outrunning a receiver is a fact the sender can act on.
|
|
1050
|
+
*/
|
|
1051
|
+
readonly mailboxDepth: 64;
|
|
1052
|
+
/**
|
|
1053
|
+
* How many times one mailbox entry may be delivered before it is dead-lettered.
|
|
1054
|
+
*
|
|
1055
|
+
* The bound exists because at-least-once and the room crash threshold compose badly on purpose.
|
|
1056
|
+
* A handler that always throws surfaces as a crash, and the supervisor's window is 3 restarts in
|
|
1057
|
+
* 60 seconds; an entry that re-armed forever would close the room in four deliveries, on every
|
|
1058
|
+
* wake, for as long as the entry existed. Five attempts is enough to ride out a transient (a
|
|
1059
|
+
* deploy mid-flight, a wake that raced a hibernate) and few enough that a genuinely poisonous
|
|
1060
|
+
* message costs a room one bad minute rather than its life. What happens then is a log line and
|
|
1061
|
+
* a drop: honest for v1, and the docs say so rather than implying a dead-letter queue exists.
|
|
1062
|
+
*/
|
|
1063
|
+
readonly maxDeliveryAttempts: 5;
|
|
1064
|
+
/**
|
|
1065
|
+
* Bus operations one room may issue per second, averaged, and the burst it may take at once.
|
|
1066
|
+
*
|
|
1067
|
+
* This is the "one hot room cannot melt the supervisor" guardrail D59 asks for. It counts both
|
|
1068
|
+
* verbs together, because both cost the supervisor a fan-out or a store write, and a limiter
|
|
1069
|
+
* that only counted the cheap one would be a limiter a room could route around.
|
|
1070
|
+
*/
|
|
1071
|
+
readonly opsPerSecond: 50;
|
|
1072
|
+
readonly opsBurst: 100;
|
|
1073
|
+
/**
|
|
1074
|
+
* How long a delivered-but-unacknowledged mailbox entry stays armed before it is delivered
|
|
1075
|
+
* again. A visibility timeout, and it is what makes at-least-once actually true rather than
|
|
1076
|
+
* nearly true.
|
|
1077
|
+
*
|
|
1078
|
+
* Posting a message to a worker is not the same as the worker running it: a room that dies
|
|
1079
|
+
* between the post and the handler (a crash, a restart, a hibernate that raced the delivery)
|
|
1080
|
+
* would otherwise lose the message with no trace, which is at-most-once wearing at-least-once's
|
|
1081
|
+
* name. So an entry is re-armed for this long at the moment it is posted, and cleared only when
|
|
1082
|
+
* the worker says the handler ran. 30 seconds is far longer than a handler takes and short
|
|
1083
|
+
* enough that a genuinely lost delivery is retried while anyone still cares.
|
|
1084
|
+
*/
|
|
1085
|
+
readonly redeliveryDelayMs: 30000;
|
|
1086
|
+
};
|
|
1087
|
+
/**
|
|
1088
|
+
* A channel name: the same shape as a room type, widened to allow dots so a project can namespace
|
|
1089
|
+
* its channels (`match.ready`, `party.invite`). Lowercase, starts alphanumeric, up to the byte
|
|
1090
|
+
* cap. Narrow on purpose: a channel name is a map key that appears in logs and in a report a
|
|
1091
|
+
* developer reads, and there is no reason for it to be able to hold arbitrary text.
|
|
1092
|
+
*/
|
|
1093
|
+
declare const BUS_CHANNEL_RE: RegExp;
|
|
1094
|
+
/**
|
|
1095
|
+
* The named failures. Each one is a thing the caller can tell apart and act on differently: a
|
|
1096
|
+
* payload the caller must shrink, a target the caller named wrong, a receiver the caller is
|
|
1097
|
+
* outrunning, and a rate the caller must slow to.
|
|
1098
|
+
*/
|
|
1099
|
+
declare const BUS_ERRORS: {
|
|
1100
|
+
readonly badChannel: "E_BUS_BAD_CHANNEL";
|
|
1101
|
+
readonly payloadTooLarge: "E_BUS_PAYLOAD_TOO_LARGE";
|
|
1102
|
+
readonly tooManySubscriptions: "E_BUS_TOO_MANY_SUBSCRIPTIONS";
|
|
1103
|
+
/** The named room does not exist in this tenant, or is a relay room and runs no handlers. */
|
|
1104
|
+
readonly noSuchRoom: "E_BUS_NO_SUCH_ROOM";
|
|
1105
|
+
readonly mailboxFull: "E_BUS_MAILBOX_FULL";
|
|
1106
|
+
readonly rateLimited: "E_BUS_RATE_LIMITED";
|
|
1107
|
+
};
|
|
1108
|
+
type BusErrorCode = (typeof BUS_ERRORS)[keyof typeof BUS_ERRORS];
|
|
1109
|
+
/** `{ code, message }` when the input is refused, `undefined` when it is fine. */
|
|
1110
|
+
interface BusProblem {
|
|
1111
|
+
readonly code: BusErrorCode;
|
|
1112
|
+
readonly message: string;
|
|
1113
|
+
}
|
|
1114
|
+
/** Validates a channel name. Shared by the runtime's courtesy check and the supervisor's real one. */
|
|
1115
|
+
declare function busChannelProblem(channel: unknown): BusProblem | undefined;
|
|
1116
|
+
/** Validates a payload. Bytes, not characters: the cap is a store and wire cost. */
|
|
1117
|
+
declare function busPayloadProblem(payload: unknown): BusProblem | undefined;
|
|
1118
|
+
/**
|
|
1119
|
+
* The alarm-name prefix a durable mailbox entry is armed under.
|
|
1120
|
+
*
|
|
1121
|
+
* Mailbox entries ride `AlarmSet`, whose `set` **replaces by name**. Two sends of the same payload
|
|
1122
|
+
* to the same room must deliver twice, so every entry gets a minted-unique name rather than a
|
|
1123
|
+
* stable one. The prefix is what lets the supervisor tell a mailbox entry from a room's own alarm
|
|
1124
|
+
* on the way out of `fireDue`, and it is not a legal `room.alarm` name (`isAlarmName` refuses a
|
|
1125
|
+
* leading `__`), so a room cannot arm one itself or cancel someone else's.
|
|
1126
|
+
*/
|
|
1127
|
+
declare const BUS_MAILBOX_PREFIX = "__bus.";
|
|
1128
|
+
/** True for an alarm name that is a mailbox entry rather than one of the room's own alarms. */
|
|
1129
|
+
declare function isMailboxAlarm(name: string): boolean;
|
|
1130
|
+
/**
|
|
1131
|
+
* What a mailbox entry's persisted payload holds. Serialised into the alarm sidecar's `payload`
|
|
1132
|
+
* field, which is why the sidecar codec had to grow one.
|
|
1133
|
+
*
|
|
1134
|
+
* `from` is stamped by the supervisor from the sending room's own record, never read off anything
|
|
1135
|
+
* the sender supplied. A room can therefore not forge a source: the field the receiver reads is
|
|
1136
|
+
* written by the only party that knows the truth.
|
|
1137
|
+
*/
|
|
1138
|
+
interface BusMailboxEntry {
|
|
1139
|
+
/** The roomId that sent this. Supervisor-stamped. */
|
|
1140
|
+
readonly from: string;
|
|
1141
|
+
readonly payload: string;
|
|
1142
|
+
/** Deliveries attempted so far, including the one in flight. Dead-lettered past the cap. */
|
|
1143
|
+
readonly attempts: number;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* M5 part 7 (D67-e): the alarm-name prefix a **cross-shard outbox** entry is armed under, on the
|
|
1147
|
+
* SENDER's own sidecar. A `send` to a room that is not on this shard parks the message here and
|
|
1148
|
+
* forwards it through the control plane; the entry is the retry and the durability, exactly as
|
|
1149
|
+
* `BUS_MAILBOX_PREFIX` is for a local delivery. Recognised by the supervisor before its generic
|
|
1150
|
+
* alarm branch, or it would reach `onAlarm` as a garbage name; a platform rolled back to a
|
|
1151
|
+
* supervisor older than this part would do exactly that, which the docs say beside the same
|
|
1152
|
+
* warning for `__bus.`. Not a legal `room.alarm` name (`isAlarmName` refuses a leading `__`).
|
|
1153
|
+
*/
|
|
1154
|
+
declare const BUS_OUTBOX_PREFIX = "__busout.";
|
|
1155
|
+
/** True for an alarm name that is a cross-shard outbox entry. */
|
|
1156
|
+
declare function isOutboxAlarm(name: string): boolean;
|
|
1157
|
+
/**
|
|
1158
|
+
* What an outbox entry's persisted payload holds. `to` is the target room; `id` is the entry's
|
|
1159
|
+
* own name, carried end to end so a target that receives the same message twice (a lost
|
|
1160
|
+
* acknowledgement) logs the duplicate by name. `firstAt` is when the message was parked, for the
|
|
1161
|
+
* one-hour dead-letter bound; `attempts` counts forwards so the retry schedule doubles.
|
|
1162
|
+
*/
|
|
1163
|
+
interface BusOutboxEntry {
|
|
1164
|
+
readonly to: string;
|
|
1165
|
+
readonly from: string;
|
|
1166
|
+
readonly payload: string;
|
|
1167
|
+
readonly id: string;
|
|
1168
|
+
readonly firstAt: number;
|
|
1169
|
+
readonly attempts: number;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* The cross-shard retry schedule (D67-e): 1, 2, 4 ... 60 seconds between forwards, for up to an
|
|
1173
|
+
* hour after the message was parked, then a dead letter in the sender's log at warn. Enforced
|
|
1174
|
+
* in the supervisor; every number is here so the docs and the code quote the same ones.
|
|
1175
|
+
*/
|
|
1176
|
+
declare const BUS_OUTBOX: {
|
|
1177
|
+
readonly firstRetryMs: 1000;
|
|
1178
|
+
readonly maxRetryMs: 60000;
|
|
1179
|
+
readonly ttlMs: number;
|
|
1180
|
+
};
|
|
1181
|
+
/** D67-e: the target's shard is not running; the control plane is placing it. Retry later. */
|
|
1182
|
+
declare const BUS_SHARD_STARTING = "E_SHARD_STARTING";
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* D60: room size classes, and the one place the mapping from a declaration to a billed class is
|
|
1186
|
+
* written down.
|
|
1187
|
+
*
|
|
1188
|
+
* A class is a *billing* concept, and saying that plainly is the whole point of this file. What is
|
|
1189
|
+
* enforced on a room is its worker heap cap, which part 2's declared sizing already handles. What
|
|
1190
|
+
* a class does is decide the rate that room's hours are charged at.
|
|
1191
|
+
*
|
|
1192
|
+
* The classes are Small, Medium and Large, with the shared relay host beneath them. Relay is not a
|
|
1193
|
+
* class: a relay room reserves no memory and holds no VM slot, so no declared `memoryMb` maps to
|
|
1194
|
+
* it. Pricing amendment 1 (2026-09-01) retired the Nano class: with the M5 RAM guardrails enforced,
|
|
1195
|
+
* a Nano room's real footprint was a VM slot plus a 32 MB heap floor, which is not meaningfully
|
|
1196
|
+
* cheaper to host than a Small room. A room that would once have been Nano is Small now, and a
|
|
1197
|
+
* schema declaring `nano` fails classification the same way any unknown class does.
|
|
1198
|
+
*
|
|
1199
|
+
* ## Why the class is derived, not declared separately
|
|
1200
|
+
*
|
|
1201
|
+
* `memoryMb` on `defineRoom` already exists, is validated to a whole 32..1024, drives the worker's
|
|
1202
|
+
* `resourceLimits`, and is summed by `checkDeclaredFit` against the VM budget. Deriving the class
|
|
1203
|
+
* from it means a project has one number to think about and the bill cannot disagree with the
|
|
1204
|
+
* enforcement. A separate `class` field could say Small while `memoryMb` said 512, and then the
|
|
1205
|
+
* invoice and the room would be telling different stories about the same room, with no way for a
|
|
1206
|
+
* developer to tell which one was the lie.
|
|
1207
|
+
*
|
|
1208
|
+
* The cost of deriving is that the thresholds are a policy that a project reads off a table rather
|
|
1209
|
+
* than states outright. That is the better trade here: the table is short, it is in the pricing
|
|
1210
|
+
* docs, and the alternative failure mode is a wrong bill.
|
|
1211
|
+
*/
|
|
1212
|
+
/** The size classes, smallest first. `small` is what every room that predates D60 is. */
|
|
1213
|
+
type RoomSizeClass = 'small' | 'medium' | 'large';
|
|
1214
|
+
/**
|
|
1215
|
+
* The floor on a declared `memoryMb`.
|
|
1216
|
+
*
|
|
1217
|
+
* The same 32 {@link DEFAULT_MEMORY_MB} is: V8's usable old-generation floor, below which a worker
|
|
1218
|
+
* cannot boot a room at all, so accepting a smaller declaration would only move the failure later.
|
|
1219
|
+
*/
|
|
1220
|
+
declare const ROOM_MEMORY_MB_MIN = 32;
|
|
1221
|
+
/**
|
|
1222
|
+
* The ceiling on a declared `memoryMb`, and the top of the Large class.
|
|
1223
|
+
*
|
|
1224
|
+
* M5 part 3.6: 1024, down from the 4096 that `VM_MEM_MIB_MAX` used to lend it by accident. Large is
|
|
1225
|
+
* *advertised* on a 1 GiB basis and priced on one, and until this cap existed a room could declare
|
|
1226
|
+
* 4096 and pay Large's rate for four times Large's memory. The under-charge was the whole band from
|
|
1227
|
+
* 1025 to 4096, silently, with the class table below saying otherwise.
|
|
1228
|
+
*
|
|
1229
|
+
* This is deliberately **not** `VM_MEM_MIB_MAX`. The two numbers answer different questions: this
|
|
1230
|
+
* one is the largest room a project may declare, and 4096 is the largest machine the platform will
|
|
1231
|
+
* build for a project's declarations. Three 1024 MB types at `maxAwake` 1 is still a legal tenant,
|
|
1232
|
+
* so the VM ceiling stays where it is.
|
|
1233
|
+
*
|
|
1234
|
+
* It lives in this file rather than beside the VM sizing constants because it is a *class*
|
|
1235
|
+
* boundary — the top of Large — and because `ROOM_CLASS_MAX_MB` below must be able to read it
|
|
1236
|
+
* without the two modules importing each other.
|
|
1237
|
+
*/
|
|
1238
|
+
declare const ROOM_MEMORY_MB_MAX = 1024;
|
|
1239
|
+
/**
|
|
1240
|
+
* The declared-memory ceiling for each class, in MB, as declared on `defineRoom`.
|
|
1241
|
+
*
|
|
1242
|
+
* `small` is 64 because that is the historical default and the rate every existing project is on,
|
|
1243
|
+
* so the boundary is placed where nothing already deployed moves class. Small is also the bottom of
|
|
1244
|
+
* the table: every declaration from V8's 32 MB floor up to 64 is Small, which is what retiring Nano
|
|
1245
|
+
* means in one line.
|
|
1246
|
+
*
|
|
1247
|
+
* `large` is 1024 because that is the 1 GiB basis Large is advertised and priced on, and because
|
|
1248
|
+
* M5 part 3.6 made it the ceiling on a declaration rather than a decoration
|
|
1249
|
+
* ({@link ROOM_MEMORY_MB_MAX}). Before that, `large` read 4096 and a room could declare four times
|
|
1250
|
+
* the memory Large's rate is derived from and pay Large's rate for it. The top of the top class and
|
|
1251
|
+
* the largest declarable room are now the same number by construction, so no band is left between
|
|
1252
|
+
* what a project may ask for and what any class is priced to cover.
|
|
1253
|
+
*/
|
|
1254
|
+
declare const ROOM_CLASS_MAX_MB: Readonly<Record<RoomSizeClass, number>>;
|
|
1255
|
+
/**
|
|
1256
|
+
* The class a room with this declared `memoryMb` is billed as.
|
|
1257
|
+
*
|
|
1258
|
+
* An undeclared room is `small`, which is the compatibility rule that matters: every project that
|
|
1259
|
+
* existed before D60 declared nothing, ran on the 64 MB default, and was billed at Small's rate,
|
|
1260
|
+
* and all three of those stay true.
|
|
1261
|
+
*/
|
|
1262
|
+
declare function roomClassFor(memoryMb: number | undefined): RoomSizeClass;
|
|
1263
|
+
/**
|
|
1264
|
+
* The `source` a class's room-hours are reported under.
|
|
1265
|
+
*
|
|
1266
|
+
* The size-class dimension rides the existing `source` column rather than a new one, and that is a
|
|
1267
|
+
* deliberate reuse rather than a shortcut. `source` is already in the usage table's primary key
|
|
1268
|
+
* (`project, meter, window_start, source, engine`), it already distinguishes writers of one meter
|
|
1269
|
+
* that must not overwrite each other, and the relay host's `$0.001` rate was already decided to
|
|
1270
|
+
* ride it. One dimension answering "what kind of thing produced this hour" is easier to reason
|
|
1271
|
+
* about than two that can disagree.
|
|
1272
|
+
*
|
|
1273
|
+
* Small stays `room`, unchanged, which is what keeps every window ever written still meaning what
|
|
1274
|
+
* it meant and keeps a Small project's bill byte-identical.
|
|
1275
|
+
*/
|
|
1276
|
+
declare function roomHoursSourceFor(cls: RoomSizeClass): string;
|
|
1277
|
+
/** The class a room-hours `source` denotes, or `undefined` when it is not a class at all. */
|
|
1278
|
+
declare function roomClassOfSource(source: string): RoomSizeClass | undefined;
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* M5 part 3.5: a tenant VM's memory is arithmetic over its room types' declarations.
|
|
1282
|
+
*
|
|
1283
|
+
* Before this file, `vmMemSizeMibFor` answered the question with two guesses: 256 MiB when any
|
|
1284
|
+
* schema declared physics, 128 MiB otherwise. Both numbers were chosen before anything measured
|
|
1285
|
+
* either of the terms they were standing in for, and part 1 measured them: the supervisor's own
|
|
1286
|
+
* resident set is roughly 53 MiB, so a 128 MiB VM has about 70 MiB for every room in it, and one
|
|
1287
|
+
* minimally-sized worker very nearly exhausts that. The 128 was not a small VM, it was a VM that
|
|
1288
|
+
* could not host a room of any size and did not say so.
|
|
1289
|
+
*
|
|
1290
|
+
* The replacement is a sum, and the whole design is that the sum's terms are things a project
|
|
1291
|
+
* *declared* rather than things the platform inferred:
|
|
1292
|
+
*
|
|
1293
|
+
* ```
|
|
1294
|
+
* S = Σ_type (memoryMb + youngGenMb) × maxAwake
|
|
1295
|
+
* + supervisorBaselineMib
|
|
1296
|
+
* + physicsHeadroomMib (once per VM, when any type declares physics)
|
|
1297
|
+
* vmMib = clamp(ceil(S × 8/7), 128, 4096)
|
|
1298
|
+
* ```
|
|
1299
|
+
*
|
|
1300
|
+
* Three things about that arithmetic are load-bearing and are stated here rather than left to be
|
|
1301
|
+
* rediscovered by whoever changes it next.
|
|
1302
|
+
*
|
|
1303
|
+
* **The native reserve is closed-form.** Part 1's derivation reserves `vmMib / 8` for off-heap
|
|
1304
|
+
* cost (worker stacks, the Rapier WASM code, socket buffers, the encode scratch). That is circular
|
|
1305
|
+
* when `vmMib` is the output rather than the input, so the reserve is solved for instead of
|
|
1306
|
+
* iterated: `vmMib = S × 8/7` makes `vmMib - S` exactly `vmMib / 8`. Pinned by
|
|
1307
|
+
* {@link nativeReserveOf} and a round-trip test — the derived worker caps, applied back to the
|
|
1308
|
+
* size this function returned, must still leave every declared worker fitting.
|
|
1309
|
+
*
|
|
1310
|
+
* **The physics headroom is measured and it is big.** A Rapier world's first step costs a
|
|
1311
|
+
* transient while V8 tier-compiles the WASM on background threads: measured in a real 1-vCPU
|
|
1312
|
+
* guest at **+97.6 MiB over pre-step RSS**, settling to +28 (part 1 report addendum, live probe
|
|
1313
|
+
* 2026-08-31). It is added once per VM rather than per type or per room, because the compile
|
|
1314
|
+
* happens once per process, and it is added at all because without it arithmetic sizing would
|
|
1315
|
+
* recreate exactly the OOM class D55 was built to fix.
|
|
1316
|
+
*
|
|
1317
|
+
* **`maxAwake` is what makes the sum well-defined.** Part 2 summed every declared type once,
|
|
1318
|
+
* because nothing could know how many rooms of a type would be awake together, and recorded the
|
|
1319
|
+
* pessimism as a debt. Multiplying by a declared concurrency is the answer that debt asked for,
|
|
1320
|
+
* and it is only honest because the supervisor *enforces* it: room N+1 of a type is refused. A
|
|
1321
|
+
* `maxAwake` nobody enforced would be a number that made the VM look big enough.
|
|
1322
|
+
*
|
|
1323
|
+
* This module is in `@irtio/protocol` because three packages need the same answer and must not
|
|
1324
|
+
* each carry their own copy of it: the host agent sizes the Firecracker machine, the supervisor
|
|
1325
|
+
* checks the size it was given against what it is about to run, and (part 7) control's
|
|
1326
|
+
* `pickServer` needs to know what a tenant will cost a box before choosing one. The fleet
|
|
1327
|
+
* protocol module is deliberately zero-dependency, which is what lets all three import it.
|
|
1328
|
+
*/
|
|
1329
|
+
|
|
1330
|
+
/** The floor. A guest kernel plus a Node process make anything smaller pointless. */
|
|
1331
|
+
declare const VM_MEM_MIB_MIN = 128;
|
|
1332
|
+
/** The ceiling. A guard against a typo booking a whole box's memory for one tenant. */
|
|
1333
|
+
declare const VM_MEM_MIB_MAX = 4096;
|
|
1334
|
+
/**
|
|
1335
|
+
* The supervisor's own resident set with zero rooms awake, in MiB, measured rather than guessed.
|
|
1336
|
+
*
|
|
1337
|
+
* Method (part 1's, re-run in part 3.5 after the baseline-shaving work): `supervisor/dist/bin.js`
|
|
1338
|
+
* booted as a child process with the placed-shaped environment `buildSupervisorEnv` emits, one
|
|
1339
|
+
* minimal room bundled by the real `bundleRoom`, six `/admin/metrics` readings over ~36 s with
|
|
1340
|
+
* `roomsByState` all zero and `sockets: 0` on every one. Rounded up to the next whole MiB.
|
|
1341
|
+
* Provenance, the raw readings and the before/after of the shaving are in
|
|
1342
|
+
* `docs/m5-part3.5-report.md`.
|
|
1343
|
+
*
|
|
1344
|
+
* This is a *constant term* in every VM's size, so a MiB here is a MiB on every tenant on every
|
|
1345
|
+
* box. Re-measure it by the same method if the supervisor's boot-time module graph changes
|
|
1346
|
+
* materially, and record the new provenance rather than nudging the number.
|
|
1347
|
+
*
|
|
1348
|
+
* **54, and most of the drop from part 1's 57 is a re-measurement rather than a saving.** Measured
|
|
1349
|
+
* on the same machine, by the same method, on both builds: the pre-shave build reads a settled max
|
|
1350
|
+
* of 53.46 MiB and the shaved build 53.20, so the lazy-import work is worth about a third of a MiB
|
|
1351
|
+
* and the other two and a half are part 1's number having been taken on a process whose first
|
|
1352
|
+
* reading had not settled. Both halves are in the report, because a constant that quietly improved
|
|
1353
|
+
* would be worse than one that says how little it moved.
|
|
1354
|
+
*/
|
|
1355
|
+
declare const SUPERVISOR_BASELINE_MIB = 54;
|
|
1356
|
+
/**
|
|
1357
|
+
* Off-heap headroom, as a fraction of the final VM size: `vmMib / NATIVE_RESERVE_FRACTION`.
|
|
1358
|
+
* A fraction rather than a constant because the things it covers — worker stacks, native
|
|
1359
|
+
* allocations, socket buffers, encode scratch — scale with what a bigger VM was made bigger for.
|
|
1360
|
+
*/
|
|
1361
|
+
declare const NATIVE_RESERVE_FRACTION = 8;
|
|
1362
|
+
/**
|
|
1363
|
+
* Added once per VM when any room type declares physics.
|
|
1364
|
+
*
|
|
1365
|
+
* 112, not the measured 97.6: the peak is what a guest must survive, and rounding a survival
|
|
1366
|
+
* margin down is how the week-9 pachinko OOM happened. The nearest power-of-two-ish number above
|
|
1367
|
+
* the peak is the honest rounding direction for a number whose failure mode is the VM dying.
|
|
1368
|
+
*/
|
|
1369
|
+
declare const PHYSICS_HEADROOM_MIB = 112;
|
|
1370
|
+
/** Every worker's young generation. Small, and scaling it buys nothing. */
|
|
1371
|
+
declare const YOUNG_GEN_MB = 32;
|
|
1372
|
+
/**
|
|
1373
|
+
* What an undeclared room type is counted at.
|
|
1374
|
+
*
|
|
1375
|
+
* 32 is V8's usable floor for an old generation and the smallest cap the platform can apply, so
|
|
1376
|
+
* counting an undeclared type at anything larger would be inventing a promise the project never
|
|
1377
|
+
* made. Together with {@link DEFAULT_MAX_AWAKE} it is the honest reading of part 1's 128 MiB
|
|
1378
|
+
* finding: one real worker, minimally sized, is what a project that declared nothing was actually
|
|
1379
|
+
* being given.
|
|
1380
|
+
*/
|
|
1381
|
+
declare const DEFAULT_MEMORY_MB = 32;
|
|
1382
|
+
/**
|
|
1383
|
+
* How many rooms of an undeclared type may be awake at once.
|
|
1384
|
+
*
|
|
1385
|
+
* One. That is the conservative reading and it is deliberately the number that makes an
|
|
1386
|
+
* undeclared single-type project size to the 128 MiB floor, which is the VM it has always had.
|
|
1387
|
+
* A project that wants more says so, and the sum grows to match, which is the entire point.
|
|
1388
|
+
*/
|
|
1389
|
+
declare const DEFAULT_MAX_AWAKE = 1;
|
|
1390
|
+
/**
|
|
1391
|
+
* The ceiling on a declared `maxAwake`.
|
|
1392
|
+
*
|
|
1393
|
+
* 256 rooms of the smallest declarable type (32 + 32 MB) is already 16 GiB before the baseline,
|
|
1394
|
+
* four times the largest VM the platform will build, so anything above it is a number that cannot
|
|
1395
|
+
* be honoured rather than a bigger project. The ceiling exists so the refusal names the mistake at
|
|
1396
|
+
* `defineRoom` time instead of at placement time, where it would be a clamp nobody saw.
|
|
1397
|
+
*/
|
|
1398
|
+
declare const MAX_AWAKE_MAX = 256;
|
|
1399
|
+
/** One room type's sizing declarations, as `defineRoom` took them. */
|
|
1400
|
+
interface RoomTypeSizing {
|
|
1401
|
+
readonly type: string;
|
|
1402
|
+
/** Declared worker old-generation cap in MB; `undefined` counts as {@link DEFAULT_MEMORY_MB}. */
|
|
1403
|
+
readonly memoryMb?: number;
|
|
1404
|
+
/** Declared concurrent awake rooms; `undefined` counts as {@link DEFAULT_MAX_AWAKE}. */
|
|
1405
|
+
readonly maxAwake?: number;
|
|
1406
|
+
/** Whether this type's schema declares physics. Adds the headroom once per VM, not per type. */
|
|
1407
|
+
readonly physics?: boolean;
|
|
1408
|
+
}
|
|
1409
|
+
/** The size, and every term that produced it, so a log line or a report can show its working. */
|
|
1410
|
+
interface VmSizing {
|
|
1411
|
+
/** The answer: what the VM is built with, after the reserve and the clamp. */
|
|
1412
|
+
readonly vmMib: number;
|
|
1413
|
+
/** Σ over the declarations, before the baseline and the headroom. */
|
|
1414
|
+
readonly workersMib: number;
|
|
1415
|
+
readonly baselineMib: number;
|
|
1416
|
+
readonly physicsHeadroomMib: number;
|
|
1417
|
+
/** `workersMib + baselineMib + physicsHeadroomMib` — the sum the reserve is solved against. */
|
|
1418
|
+
readonly preReserveMib: number;
|
|
1419
|
+
/** `vmMib - preReserveMib`, which is `vmMib / 8` whenever the clamp did not bite. */
|
|
1420
|
+
readonly nativeReserveMib: number;
|
|
1421
|
+
/** `'floor'` / `'ceiling'` when the clamp moved the answer, else `undefined`. */
|
|
1422
|
+
readonly clamped?: 'floor' | 'ceiling';
|
|
1423
|
+
/** One line naming the arithmetic, for a boot log or an error. */
|
|
1424
|
+
readonly detail: string;
|
|
1425
|
+
}
|
|
1426
|
+
/** The reserve a VM of this size carries: one eighth, floored. */
|
|
1427
|
+
declare function nativeReserveOf(vmMib: number): number;
|
|
1428
|
+
/** What one declared type contributes to the sum: `(memoryMb + youngGen) × maxAwake`. */
|
|
1429
|
+
declare function typeFootprintMib(type: RoomTypeSizing): number;
|
|
1430
|
+
/**
|
|
1431
|
+
* The VM size a project's declarations ask for.
|
|
1432
|
+
*
|
|
1433
|
+
* A project with no room types at all — a relay-only tenant, which still gets a VM until D66's
|
|
1434
|
+
* second half lands — sizes to the floor. So does an undeclared single-type project, which is
|
|
1435
|
+
* the compatibility-shaped case even though compatibility is not what is being kept here: it is
|
|
1436
|
+
* the size that project has always had, and the arithmetic agrees with it rather than being made
|
|
1437
|
+
* to.
|
|
1438
|
+
*/
|
|
1439
|
+
declare function vmSizeFor(types: readonly RoomTypeSizing[], options?: {
|
|
1440
|
+
readonly baselineMib?: number;
|
|
1441
|
+
}): VmSizing;
|
|
1442
|
+
/**
|
|
1443
|
+
* How many awake rooms of each class one vCPU is expected to carry.
|
|
1444
|
+
*
|
|
1445
|
+
* The CPU allowance is arithmetic over declarations for the same reason the memory size is: the
|
|
1446
|
+
* alternative is a number chosen from a marketing table that the throttle then has to pretend to
|
|
1447
|
+
* honour. A project declares `memoryMb` and `maxAwake`, the class comes from the memory, and the
|
|
1448
|
+
* quota comes from the sum. One number decides the class, the bill, the heap cap and now the CPU
|
|
1449
|
+
* share, so none of them can disagree with the others.
|
|
1450
|
+
*
|
|
1451
|
+
* **Where these came from.** Small and Large are the owner's anchors. Medium is 8 rather than
|
|
1452
|
+
* the plan's 10, **set from measurement and owner-reviewable at merge**: a density bench against a
|
|
1453
|
+
* real supervisor with real WebSocket clients measured marginal CPU per room on the full
|
|
1454
|
+
* supervisor-and-network path at 1.2% for a casual tick room, 2.3% for an active one, and 7.1% for
|
|
1455
|
+
* a physics room of 60 Rapier bodies. That is about 8.5 physics rooms per core at 60% utilisation
|
|
1456
|
+
* on the bench machine, and the production box is slower per core, so 10 would have over-promised
|
|
1457
|
+
* for exactly the rooms that land in Medium by default. The bench numbers and their caveats are in
|
|
1458
|
+
* `docs/m5-part3.6-report.md`.
|
|
1459
|
+
*
|
|
1460
|
+
* Keep them here, in one table, so an owner adjustment is one line rather than an audit.
|
|
1461
|
+
*/
|
|
1462
|
+
declare const ROOMS_PER_VCPU: Readonly<Record<RoomSizeClass, number>>;
|
|
1463
|
+
/** The floor on a derived quota, as a percentage of one vCPU. Below this a room cannot serve. */
|
|
1464
|
+
declare const CPU_QUOTA_PCT_MIN = 10;
|
|
1465
|
+
/** The ceiling: one whole vCPU, which is all a Firecracker guest is built with. */
|
|
1466
|
+
declare const CPU_QUOTA_PCT_MAX = 100;
|
|
1467
|
+
/** A tenant's CPU allowance, and the terms that produced it. */
|
|
1468
|
+
interface VmCpuSizing {
|
|
1469
|
+
/** The answer: percent of one vCPU, for `systemctl set-property … CPUQuota=<n>%`. */
|
|
1470
|
+
readonly quotaPct: number;
|
|
1471
|
+
/** The sum before the clamp, so a log line can show an over-declared project its own number. */
|
|
1472
|
+
readonly rawPct: number;
|
|
1473
|
+
/** `true` when the project declared nothing at all and is therefore not throttled. */
|
|
1474
|
+
readonly undeclared: boolean;
|
|
1475
|
+
/** `'floor'` / `'ceiling'` when the clamp moved the answer, else `undefined`. */
|
|
1476
|
+
readonly clamped?: 'floor' | 'ceiling';
|
|
1477
|
+
/** One line naming the arithmetic, for a boot log. */
|
|
1478
|
+
readonly detail: string;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* The CPU quota a project's declarations ask for, as a percentage of the VM's one vCPU.
|
|
1482
|
+
*
|
|
1483
|
+
* ```
|
|
1484
|
+
* rawPct = Σ_type ceil-free (maxAwake × 100 / roomsPerVcpu(class(memoryMb)))
|
|
1485
|
+
* quotaPct = clamp(ceil(rawPct), 10, 100)
|
|
1486
|
+
* ```
|
|
1487
|
+
*
|
|
1488
|
+
* **An undeclared project gets no throttle**, which is the same posture `maxAwake` enforcement took
|
|
1489
|
+
* in part 3.5 and is stated here because it is the rule most likely to be read as an oversight.
|
|
1490
|
+
* Only declarations are held against a project. A project that declared nothing made no promise
|
|
1491
|
+
* about its concurrency, so there is no sum to derive a share from, and inventing one would throttle
|
|
1492
|
+
* every tenant that predates this function on a number it never said.
|
|
1493
|
+
*
|
|
1494
|
+
* **The quota is a ceiling, not a reservation.** `CPUQuota=` bounds what a VM may take; it does not
|
|
1495
|
+
* hold anything back for it. So a project that declares more concurrency than its class density
|
|
1496
|
+
* supports is not taking CPU from its neighbours on the box, it is spreading its own allowance
|
|
1497
|
+
* thinner across its own rooms. The failure mode of an optimistic density is a project's own rooms
|
|
1498
|
+
* ticking late, which is the right place for it to land.
|
|
1499
|
+
*
|
|
1500
|
+
* The class comes from {@link roomClassFor} over the same `memoryMb` that decides the bill, so the
|
|
1501
|
+
* CPU share and the invoice cannot tell different stories about one room.
|
|
1502
|
+
*/
|
|
1503
|
+
declare function vmCpuFor(types: readonly RoomTypeSizing[]): VmCpuSizing;
|
|
1504
|
+
/**
|
|
1505
|
+
* Does what a project declared fit a VM of this size?
|
|
1506
|
+
*
|
|
1507
|
+
* Under arithmetic sizing this can only be false one way: a project set an explicit `vmMemMib`
|
|
1508
|
+
* override smaller than its own declarations. That is why the supervisor treats a false answer as
|
|
1509
|
+
* a refusal to boot rather than a log line. Pre-release, an override that lies about the machine
|
|
1510
|
+
* its rooms are running in should fail where somebody is looking.
|
|
1511
|
+
*/
|
|
1512
|
+
declare function declarationsFit(vmMemMib: number, types: readonly RoomTypeSizing[], options?: {
|
|
1513
|
+
readonly baselineMib?: number;
|
|
1514
|
+
}): {
|
|
1515
|
+
readonly fits: boolean;
|
|
1516
|
+
readonly required: VmSizing;
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Room retention: how long a room outlives its last activity, as a room type declares it.
|
|
1521
|
+
*
|
|
1522
|
+
* A room type declares `retention: '10m'` beside `memoryMb` and `maxAwake`, and control's reaper
|
|
1523
|
+
* deletes the room's stored objects once that long has passed since the room last hibernated.
|
|
1524
|
+
* Absent means forever, which is what every room has always had and stays the default.
|
|
1525
|
+
*
|
|
1526
|
+
* **Why a string and not a number of milliseconds.** Every other duration in this repository is a
|
|
1527
|
+
* `durationMs` number, and this one deliberately is not. The declaration is read by a human in a
|
|
1528
|
+
* room file and it is the one number in that file whose mistakes delete data: `retention: 600000`
|
|
1529
|
+
* and `retention: 600_000_000` look alike at a glance, and `'10m'` and `'10000m'` do not. The
|
|
1530
|
+
* parsed number never reaches storage either — control stores the declared string, so a row can
|
|
1531
|
+
* always be read back as the thing the developer wrote.
|
|
1532
|
+
*
|
|
1533
|
+
* **The grammar is narrow on purpose.** One integer, one unit, no space, no fraction, no compound
|
|
1534
|
+
* (`'1h30m'`), no seconds. A window is a coarse promise about when data goes away and the sweep
|
|
1535
|
+
* cadence is its real resolution, so a grammar that can express a precision the platform does not
|
|
1536
|
+
* have would be a lie in the type system. Anything outside it is refused by name at every door
|
|
1537
|
+
* rather than coerced: a retention that was silently rounded, clamped or defaulted is the exact
|
|
1538
|
+
* failure mode a deletion feature must not have.
|
|
1539
|
+
*
|
|
1540
|
+
* This lives in `@irtio/protocol` because it is a declaration constant shared by more than one
|
|
1541
|
+
* package, which is what `sizing.ts` and `classes.ts` are already here for. `@irtio/server` is on
|
|
1542
|
+
* the room-bundle side of the fence and may not import it, so it carries the same grammar and the
|
|
1543
|
+
* same bounds as literals, pinned equal by `packages/supervisor/test/declaration-doors.test.ts`.
|
|
1544
|
+
*/
|
|
1545
|
+
/**
|
|
1546
|
+
* The declared form: a positive integer of up to five digits, then one of `m`, `h`, `d`.
|
|
1547
|
+
*
|
|
1548
|
+
* Five digits because the ceiling (3650 days) needs four in days, five in hours, and seven in
|
|
1549
|
+
* minutes would be past the ceiling anyway — the bounds check below is what actually decides, and
|
|
1550
|
+
* the digit cap only exists so a pathological input cannot be a number at all.
|
|
1551
|
+
*/
|
|
1552
|
+
declare const RETENTION_RE: RegExp;
|
|
1553
|
+
/** One minute. Below this the sweep cadence (15 minutes by default) is the whole window. */
|
|
1554
|
+
declare const RETENTION_MIN_MS = 60000;
|
|
1555
|
+
/** Ten years. A declaration above this is asking for "forever", which is written by omitting it. */
|
|
1556
|
+
declare const RETENTION_MAX_MS: number;
|
|
1557
|
+
/**
|
|
1558
|
+
* The window in milliseconds, or `undefined` when `raw` is not a legal declaration.
|
|
1559
|
+
*
|
|
1560
|
+
* `undefined` is the single answer for every rejection — malformed, out of range, wrong type —
|
|
1561
|
+
* because every caller does the same thing with it: refuse by name, naming the value it was given.
|
|
1562
|
+
* A caller that needs to say *why* has the value in hand and the bounds exported beside this.
|
|
1563
|
+
*/
|
|
1564
|
+
declare function parseRetention(raw: unknown): number | undefined;
|
|
1565
|
+
|
|
387
1566
|
/**
|
|
388
1567
|
* Browser-origin policy, shared by every component that has to answer "may this page connect?"
|
|
389
1568
|
*
|
|
@@ -432,4 +1611,4 @@ declare function originAllowed(origins: readonly string[], allowNoOrigin: boolea
|
|
|
432
1611
|
*/
|
|
433
1612
|
declare function parseOriginList(raw: string | undefined): readonly string[];
|
|
434
1613
|
|
|
435
|
-
export { type Call, type ClientCallable, type Credential, ERROR_CATALOGUE, type ErrorCatalogueEntry, ErrorCode, type ErrorCodeDef, type ErrorCodeName, type ErrorPayload, type Frame, FrameType, type Hello, type Msg, type MsgTarget, PRESENCE_COLLECTION, PROTOCOL_VERSION, type Ping, type Pong, type PresenceRecord, RELAY_HASH8, type Reply, type Welcome, builtinRpcs, correctPayload, decodeCall, decodeErrorPayload, decodeFrame, decodeHello, decodeMsg, decodePing, decodePong, decodeReply, decodeWelcome, deltaPayload, encodeCall, encodeCorrectFrame, encodeDeltaFrame, encodeErrorPayload, encodeFrame, encodeHello, encodeMsg, encodePing, encodePong, encodeReply, encodeWelcome, encodeWriteFrame, errorByCode, formatError, isFrameType, isLocalhostOrigin, isRelayHash8, originAllowed, parseOriginList, presenceEntity, readCorrectAppliedTick, readCorrectClientTick, relaySchema, requestOwnership, rpcByIdOf, rpcIdOf, rpcTable, withBuiltins, writePayload };
|
|
1614
|
+
export { type AttributeOptions, BUS_CHANNEL_RE, BUS_ERRORS, BUS_LIMITS, BUS_MAILBOX_PREFIX, BUS_OUTBOX, BUS_OUTBOX_PREFIX, BUS_SHARD_STARTING, type BusErrorCode, type BusMailboxEntry, type BusOutboxEntry, type BusProblem, CLOSE_EGRESS_WALL, CLOSE_ROOM_DELETED, CLOSE_TRY_AGAIN_LATER, CODE_ALPHABET, CPU_QUOTA_PCT_MAX, CPU_QUOTA_PCT_MIN, type Call, type ChurnIds, type ClientCallable, type Credential, DEFAULT_MAX_AWAKE, DEFAULT_MEMORY_MB, DEFAULT_ROOM_TYPE, EMPTY_PROFILE, ERROR_CATALOGUE, type ErrorCatalogueEntry, ErrorCode, type ErrorCodeDef, type ErrorCodeName, type ErrorPayload, type Frame, FrameType, HELLO_SCHEMA_SWAP_BIT, type Hello, MATCH_CODE_LENGTH, MAX_AWAKE_MAX, MSG_KIND_TYPED, type Msg, type MsgTarget, NATIVE_RESERVE_FRACTION, PHYSICS_HEADROOM_MIB, PRESENCE_COLLECTION, PROFILE_KINDS, PROTOCOL_VERSION, type ParsedRoomId, type Ping, type Pong, type PresenceRecord, type ProfileKind, ProfileLedger, type ProfileRow, type ProfileSnapshot, RELAY_HASH8, RETENTION_MAX_MS, RETENTION_MIN_MS, RETENTION_RE, ROOMS_PER_VCPU, ROOM_CLASS_MAX_MB, ROOM_ID_RE, ROOM_MEMORY_MB_MAX, ROOM_MEMORY_MB_MIN, ROOM_TYPE_DELIMITER, ROOM_TYPE_RE, type Reply, type RoomSizeClass, type RoomTypeSizing, SUPERVISOR_BASELINE_MIB, VM_MEM_MIB_MAX, VM_MEM_MIB_MIN, type VmCpuSizing, type VmSizing, type VoiceClientMessage, type VoicePeer, type VoiceServerMessage, type VoiceTransportInfo, type Welcome, YOUNG_GEN_MB, builtinRpcs, busChannelProblem, busPayloadProblem, correctPayload, declarationsFit, decodeCall, decodeErrorPayload, decodeFrame, decodeHello, decodeMsg, decodePing, decodePong, decodeReply, decodeVoiceMessage, decodeWelcome, deltaPayload, diffProfiles, encodeCall, encodeCorrectFrame, encodeDeltaFrame, encodeErrorPayload, encodeFrame, encodeHello, encodeMsg, encodePing, encodePong, encodeReply, encodeSchemaFrame, encodeVoiceMessage, encodeWelcome, encodeWriteFrame, errorByCode, formatError, formatRoomId, isFrameType, isLocalhostOrigin, isMailboxAlarm, isOutboxAlarm, isRelayHash8, isTypedMsg, isVoiceMsg, mergeProfiles, nativeReserveOf, originAllowed, parseOriginList, parseRetention, parseRoomId, presenceEntity, readCorrectAppliedTick, readCorrectClientTick, readSchemaPayload, relaySchema, requestOwnership, roomClassFor, roomClassOfSource, roomHoursSourceFor, rpcByIdOf, rpcIdOf, rpcTable, scaleProfile, schemaPayload, topProfileRows, typeFootprintMib, typedMsgFromClientOk, vmCpuFor, vmSizeFor, withBuiltins, writePayload };
|