@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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/frame.ts
|
|
2
|
-
var PROTOCOL_VERSION =
|
|
2
|
+
var PROTOCOL_VERSION = 3;
|
|
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
|
|
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,94 @@ 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
|
+
},
|
|
152
|
+
// ---- M6 lane A: rooms API ----
|
|
153
|
+
/**
|
|
154
|
+
* M6 lane A (D68-d): an operator deleted this room, and its stored state is gone.
|
|
155
|
+
*
|
|
156
|
+
* Its own code rather than `E_KICKED` or `E_ROOM_CLOSED`, and the difference between the three is
|
|
157
|
+
* the reason it exists. `E_KICKED` is the room's own code deciding one player should leave, and
|
|
158
|
+
* a client's right answer is usually to show a message and stay in the game. `E_ROOM_CLOSED` is
|
|
159
|
+
* a room ending normally; rejoining makes a fresh one and nothing was lost. This is neither: the
|
|
160
|
+
* room's state has been deleted from outside the game, deliberately, by somebody with a rooms
|
|
161
|
+
* API credential. A client that reconnects gets an empty room rather than the one it was in.
|
|
162
|
+
* Different thing to tell a player, different thing for a client to do, different code.
|
|
163
|
+
*
|
|
164
|
+
* Fatal, which is load-bearing: `@irtio/client`'s fatal path leaves for good rather than
|
|
165
|
+
* reconnecting, and that is what stops a deleted room from being immediately recreated by the
|
|
166
|
+
* very clients that were in it. Close code {@link CLOSE_ROOM_DELETED}.
|
|
167
|
+
*/
|
|
168
|
+
E_ROOM_DELETED: {
|
|
169
|
+
code: 34,
|
|
170
|
+
message: "room {roomId} was deleted by an operator; its stored state is gone"
|
|
171
|
+
}
|
|
172
|
+
// ---- end M6 lane A ----
|
|
78
173
|
};
|
|
174
|
+
var CLOSE_TRY_AGAIN_LATER = 1013;
|
|
175
|
+
var CLOSE_EGRESS_WALL = 4290;
|
|
176
|
+
var CLOSE_ROOM_DELETED = 4291;
|
|
79
177
|
var ERROR_CATALOGUE = Object.keys(ErrorCode).map((name) => ({ name, code: ErrorCode[name].code, message: ErrorCode[name].message }));
|
|
80
178
|
var BY_CODE = new Map(
|
|
81
179
|
ERROR_CATALOGUE.map((e) => [e.code, e])
|
|
@@ -101,6 +199,7 @@ function assertEof(r, what) {
|
|
|
101
199
|
var HELLO_RESUME_BIT = 1 << 0;
|
|
102
200
|
var HELLO_ROLE_BIT = 1 << 1;
|
|
103
201
|
var HELLO_NAME_BIT = 1 << 2;
|
|
202
|
+
var HELLO_SCHEMA_SWAP_BIT = 1 << 3;
|
|
104
203
|
function encodeHello(h) {
|
|
105
204
|
if (h.schemaHash8.length !== 8) {
|
|
106
205
|
throw new Error(`encodeHello: schemaHash8 must be 8 bytes, got ${h.schemaHash8.length}`);
|
|
@@ -114,7 +213,7 @@ function encodeHello(h) {
|
|
|
114
213
|
w.u8(1);
|
|
115
214
|
w.str(h.credential.token);
|
|
116
215
|
} else {
|
|
117
|
-
w.u8(2);
|
|
216
|
+
w.u8(h.credential.kind === "assertion" ? 3 : 2);
|
|
118
217
|
w.str(h.credential.key);
|
|
119
218
|
w.str(h.credential.token);
|
|
120
219
|
}
|
|
@@ -124,6 +223,7 @@ function encodeHello(h) {
|
|
|
124
223
|
if (h.resumeToken !== void 0) mask |= HELLO_RESUME_BIT;
|
|
125
224
|
if (h.role !== void 0) mask |= HELLO_ROLE_BIT;
|
|
126
225
|
if (h.name !== void 0) mask |= HELLO_NAME_BIT;
|
|
226
|
+
if (h.schemaSwap === true) mask |= HELLO_SCHEMA_SWAP_BIT;
|
|
127
227
|
w.u8(mask);
|
|
128
228
|
if (h.resumeToken !== void 0) w.str(h.resumeToken);
|
|
129
229
|
if (h.role !== void 0) w.str(h.role);
|
|
@@ -138,6 +238,7 @@ function decodeHello(bytes) {
|
|
|
138
238
|
if (credKind === 0) credential = { kind: "key", key: r.str() };
|
|
139
239
|
else if (credKind === 1) credential = { kind: "token", token: r.str() };
|
|
140
240
|
else if (credKind === 2) credential = { kind: "jwt", key: r.str(), token: r.str() };
|
|
241
|
+
else if (credKind === 3) credential = { kind: "assertion", key: r.str(), token: r.str() };
|
|
141
242
|
else throw new Error(`decodeHello: unknown credential kind ${credKind}`);
|
|
142
243
|
const roomId = r.str();
|
|
143
244
|
const schemaHash8 = r.bytes(8);
|
|
@@ -145,13 +246,15 @@ function decodeHello(bytes) {
|
|
|
145
246
|
const resumeToken = (mask & HELLO_RESUME_BIT) !== 0 ? r.str() : void 0;
|
|
146
247
|
const role = (mask & HELLO_ROLE_BIT) !== 0 ? r.str() : void 0;
|
|
147
248
|
const name = (mask & HELLO_NAME_BIT) !== 0 ? r.str() : void 0;
|
|
249
|
+
const schemaSwap = (mask & HELLO_SCHEMA_SWAP_BIT) !== 0;
|
|
148
250
|
assertEof(r, "decodeHello");
|
|
149
251
|
const hello = { protocolVersion, credential, roomId, schemaHash8 };
|
|
150
252
|
return {
|
|
151
253
|
...hello,
|
|
152
254
|
...resumeToken !== void 0 ? { resumeToken } : {},
|
|
153
255
|
...role !== void 0 ? { role } : {},
|
|
154
|
-
...name !== void 0 ? { name } : {}
|
|
256
|
+
...name !== void 0 ? { name } : {},
|
|
257
|
+
...schemaSwap ? { schemaSwap: true } : {}
|
|
155
258
|
};
|
|
156
259
|
}
|
|
157
260
|
function encodeWelcome(w0) {
|
|
@@ -162,7 +265,8 @@ function encodeWelcome(w0) {
|
|
|
162
265
|
w.blob(w0.snapshot);
|
|
163
266
|
w.str(w0.resumeToken);
|
|
164
267
|
w.str(w0.roomId);
|
|
165
|
-
w.u16(w0.
|
|
268
|
+
w.u16(w0.tickRate);
|
|
269
|
+
w.u16(w0.maxClients);
|
|
166
270
|
return w.finish();
|
|
167
271
|
}
|
|
168
272
|
function decodeWelcome(bytes) {
|
|
@@ -173,9 +277,10 @@ function decodeWelcome(bytes) {
|
|
|
173
277
|
const snapshot = r.blob();
|
|
174
278
|
const resumeToken = r.str();
|
|
175
279
|
const roomId = r.str();
|
|
176
|
-
const
|
|
280
|
+
const tickRate = r.eof ? 0 : r.u16();
|
|
281
|
+
const maxClients = r.eof ? 0 : r.u16();
|
|
177
282
|
assertEof(r, "decodeWelcome");
|
|
178
|
-
return { clientId, role, tick, snapshot, resumeToken, roomId,
|
|
283
|
+
return { clientId, role, tick, snapshot, resumeToken, roomId, tickRate, maxClients };
|
|
179
284
|
}
|
|
180
285
|
function encodeErrorPayload(e) {
|
|
181
286
|
const w = new ByteWriter();
|
|
@@ -245,6 +350,15 @@ function encodeWriteFrame(codecBytes) {
|
|
|
245
350
|
function encodeCorrectFrame(codecBytes, clientTick, appliedTick) {
|
|
246
351
|
return encodeFrame(FrameType.CORRECT, correctPayload(codecBytes, clientTick, appliedTick));
|
|
247
352
|
}
|
|
353
|
+
function schemaPayload(canonical) {
|
|
354
|
+
return new TextEncoder().encode(canonical);
|
|
355
|
+
}
|
|
356
|
+
function readSchemaPayload(payload) {
|
|
357
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
358
|
+
}
|
|
359
|
+
function encodeSchemaFrame(canonical) {
|
|
360
|
+
return encodeFrame(FrameType.SCHEMA, schemaPayload(canonical));
|
|
361
|
+
}
|
|
248
362
|
|
|
249
363
|
// src/rpc.ts
|
|
250
364
|
import { ByteReader as ByteReader2, ByteWriter as ByteWriter2, bool, server, str } from "@irtio/schema";
|
|
@@ -255,6 +369,7 @@ function encodeCall(c) {
|
|
|
255
369
|
const w = new ByteWriter2();
|
|
256
370
|
w.u32(c.reqId);
|
|
257
371
|
w.u16(c.rpcId);
|
|
372
|
+
w.u32(c.clientTick ?? 0);
|
|
258
373
|
w.bytes(c.params);
|
|
259
374
|
return w.finish();
|
|
260
375
|
}
|
|
@@ -262,8 +377,9 @@ function decodeCall(bytes) {
|
|
|
262
377
|
const r = new ByteReader2(bytes);
|
|
263
378
|
const reqId = r.u32();
|
|
264
379
|
const rpcId = r.u16();
|
|
380
|
+
const clientTick = r.u32();
|
|
265
381
|
const params = r.rest();
|
|
266
|
-
return { reqId, rpcId, params };
|
|
382
|
+
return { reqId, rpcId, clientTick, params };
|
|
267
383
|
}
|
|
268
384
|
function encodeReply(r) {
|
|
269
385
|
const w = new ByteWriter2();
|
|
@@ -323,36 +439,505 @@ function rpcByIdOf(schema, id) {
|
|
|
323
439
|
return found;
|
|
324
440
|
}
|
|
325
441
|
|
|
442
|
+
// src/profile.ts
|
|
443
|
+
import { ByteReader as ByteReader3, walkDelta, walkSnapshot } from "@irtio/schema";
|
|
444
|
+
|
|
445
|
+
// src/presence.ts
|
|
446
|
+
import { bool as bool2, defineSchema, entity, str as str2 } from "@irtio/schema";
|
|
447
|
+
var presenceEntity = entity(
|
|
448
|
+
{ clientId: str2(32), role: str2(32), name: str2(32), connected: bool2 },
|
|
449
|
+
{ serverOwned: true }
|
|
450
|
+
);
|
|
451
|
+
var PRESENCE_COLLECTION = "clients";
|
|
452
|
+
var extended = /* @__PURE__ */ new WeakMap();
|
|
453
|
+
function withBuiltins(schema) {
|
|
454
|
+
let ext = extended.get(schema);
|
|
455
|
+
if (!ext) {
|
|
456
|
+
ext = defineSchema(
|
|
457
|
+
{ ...schema.defs, [PRESENCE_COLLECTION]: presenceEntity },
|
|
458
|
+
{
|
|
459
|
+
rpc: schema.rpc,
|
|
460
|
+
roles: schema.roles,
|
|
461
|
+
// D70: message shapes travel with the extension. The builder's hash is what HELLO carries
|
|
462
|
+
// and what a typed message is validated against, but the runtime reaches its schema
|
|
463
|
+
// through `ext` in several places, and an extension that quietly dropped `messages` would
|
|
464
|
+
// make a declared message decodable on one side of the room and not the other.
|
|
465
|
+
...schema.messageDefs !== void 0 && Object.keys(schema.messageDefs).length > 0 ? { messages: schema.messageDefs } : {},
|
|
466
|
+
...schema.project !== void 0 ? { project: schema.project } : {},
|
|
467
|
+
allowReservedNames: true
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
extended.set(schema, ext);
|
|
471
|
+
}
|
|
472
|
+
return ext;
|
|
473
|
+
}
|
|
474
|
+
var relaySchema = withBuiltins(defineSchema({}));
|
|
475
|
+
var RELAY_HASH8 = new Uint8Array(8);
|
|
476
|
+
function isRelayHash8(hash8) {
|
|
477
|
+
if (hash8.length !== 8) return false;
|
|
478
|
+
for (const b of hash8) if (b !== 0) return false;
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/profile.ts
|
|
483
|
+
var PROFILE_KINDS = [
|
|
484
|
+
"field",
|
|
485
|
+
"presence",
|
|
486
|
+
"correction",
|
|
487
|
+
"write",
|
|
488
|
+
"churn",
|
|
489
|
+
"rpc",
|
|
490
|
+
"message",
|
|
491
|
+
"voice",
|
|
492
|
+
"join",
|
|
493
|
+
"overhead",
|
|
494
|
+
"control"
|
|
495
|
+
];
|
|
496
|
+
var EMPTY_PROFILE = {
|
|
497
|
+
rows: [],
|
|
498
|
+
bytesIn: 0,
|
|
499
|
+
bytesOut: 0,
|
|
500
|
+
framesIn: 0,
|
|
501
|
+
framesOut: 0,
|
|
502
|
+
walks: 0
|
|
503
|
+
};
|
|
504
|
+
var CONTROL_KEYS = {
|
|
505
|
+
[FrameType.HELLO]: "hello",
|
|
506
|
+
[FrameType.WELCOME]: "welcome",
|
|
507
|
+
[FrameType.ERROR]: "error",
|
|
508
|
+
[FrameType.PING]: "ping",
|
|
509
|
+
[FrameType.PONG]: "pong",
|
|
510
|
+
[FrameType.LEAVE]: "leave",
|
|
511
|
+
[FrameType.SCHEMA]: "schema"
|
|
512
|
+
};
|
|
513
|
+
var MSG_KEYS = ["all", "client", "role", "server"];
|
|
514
|
+
var PENDING_CALLS_MAX = 512;
|
|
515
|
+
var ProfileLedger = class {
|
|
516
|
+
rows = /* @__PURE__ */ new Map();
|
|
517
|
+
memo = /* @__PURE__ */ new Map();
|
|
518
|
+
/** Peer and reqId to the rpc name its CALL carried, so the matching REPLY can be named too. */
|
|
519
|
+
pending = /* @__PURE__ */ new Map();
|
|
520
|
+
scratch = [];
|
|
521
|
+
bytesIn = 0;
|
|
522
|
+
bytesOut = 0;
|
|
523
|
+
framesIn = 0;
|
|
524
|
+
framesOut = 0;
|
|
525
|
+
walkCount = 0;
|
|
526
|
+
rpcSchema;
|
|
527
|
+
/**
|
|
528
|
+
* `schema` is the runtime-extended schema (`withBuiltins`), which is what delta and snapshot
|
|
529
|
+
* bodies are encoded against. `rpcSchema` is the one whose `rpcTable` the `rpcId` on the wire
|
|
530
|
+
* indexes; the two tables agree today (extending adds a collection, not an rpc), but the
|
|
531
|
+
* senders name them separately and so does this.
|
|
532
|
+
*/
|
|
533
|
+
schema;
|
|
534
|
+
constructor(schema, rpcSchema) {
|
|
535
|
+
this.schema = schema;
|
|
536
|
+
this.rpcSchema = rpcSchema ?? schema;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* D50: a session whose schema was swapped mid-flight keeps its ledger and its accumulated rows.
|
|
540
|
+
* Rows named after a field the new schema dropped simply stop growing, which is the honest
|
|
541
|
+
* reading — those bytes really were spent.
|
|
542
|
+
*/
|
|
543
|
+
swapSchema(schema, rpcSchema) {
|
|
544
|
+
this.schema = schema;
|
|
545
|
+
this.rpcSchema = rpcSchema ?? schema;
|
|
546
|
+
this.memo.clear();
|
|
547
|
+
}
|
|
548
|
+
get walks() {
|
|
549
|
+
return this.walkCount;
|
|
550
|
+
}
|
|
551
|
+
/** Drops the shared-payload memo. Call at the top of each flush. */
|
|
552
|
+
newFlush() {
|
|
553
|
+
if (this.memo.size > 0) this.memo.clear();
|
|
554
|
+
}
|
|
555
|
+
reset() {
|
|
556
|
+
this.rows.clear();
|
|
557
|
+
this.memo.clear();
|
|
558
|
+
this.pending.clear();
|
|
559
|
+
this.bytesIn = 0;
|
|
560
|
+
this.bytesOut = 0;
|
|
561
|
+
this.framesIn = 0;
|
|
562
|
+
this.framesOut = 0;
|
|
563
|
+
this.walkCount = 0;
|
|
564
|
+
}
|
|
565
|
+
snapshot() {
|
|
566
|
+
const rows = [];
|
|
567
|
+
for (const kind of PROFILE_KINDS) {
|
|
568
|
+
const byKey = this.rows.get(kind);
|
|
569
|
+
if (!byKey) continue;
|
|
570
|
+
for (const [key, v] of byKey) rows.push({ kind, key, out: v.out, in: v.in });
|
|
571
|
+
}
|
|
572
|
+
rows.sort((a, b) => b.out + b.in - (a.out + a.in) || (a.key < b.key ? -1 : 1));
|
|
573
|
+
return {
|
|
574
|
+
rows,
|
|
575
|
+
bytesIn: this.bytesIn,
|
|
576
|
+
bytesOut: this.bytesOut,
|
|
577
|
+
framesIn: this.framesIn,
|
|
578
|
+
framesOut: this.framesOut,
|
|
579
|
+
walks: this.walkCount
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Attributes one whole frame, envelope byte included. Never throws: a frame this ledger cannot
|
|
584
|
+
* read still contributes its full length, to `overhead/unattributed`.
|
|
585
|
+
*/
|
|
586
|
+
attribute(dir, frame, options) {
|
|
587
|
+
if (dir === "in") {
|
|
588
|
+
this.framesIn++;
|
|
589
|
+
this.bytesIn += frame.length;
|
|
590
|
+
} else {
|
|
591
|
+
this.framesOut++;
|
|
592
|
+
this.bytesOut += frame.length;
|
|
593
|
+
}
|
|
594
|
+
const shared = options?.shared;
|
|
595
|
+
if (shared !== void 0) {
|
|
596
|
+
const cached = this.memo.get(shared);
|
|
597
|
+
if (cached) {
|
|
598
|
+
this.commit(dir, cached);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const entries = shared !== void 0 ? [] : this.scratch;
|
|
603
|
+
entries.length = 0;
|
|
604
|
+
this.walkCount++;
|
|
605
|
+
this.describe(frame, entries, options);
|
|
606
|
+
const total = sum(entries);
|
|
607
|
+
if (total !== frame.length) {
|
|
608
|
+
entries.push({
|
|
609
|
+
kind: "overhead",
|
|
610
|
+
key: "unattributed",
|
|
611
|
+
bytes: frame.length - total
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (shared !== void 0) this.memo.set(shared, entries);
|
|
615
|
+
this.commit(dir, entries);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Attributes bare snapshot bytes under `join`. The room worker needs this: it hands the host
|
|
619
|
+
* snapshot bytes and the host wraps them in a `WELCOME`, so the room never sees that frame,
|
|
620
|
+
* but the bytes are still the room's egress and the join rows are the reason anyone profiles a
|
|
621
|
+
* join at all. A client, which does see the whole frame, uses `attribute` instead.
|
|
622
|
+
*/
|
|
623
|
+
attributeSnapshot(dir, bytes) {
|
|
624
|
+
if (dir === "in") {
|
|
625
|
+
this.bytesIn += bytes.length;
|
|
626
|
+
} else {
|
|
627
|
+
this.bytesOut += bytes.length;
|
|
628
|
+
}
|
|
629
|
+
const entries = [];
|
|
630
|
+
this.walkCount++;
|
|
631
|
+
try {
|
|
632
|
+
this.snapshotBody(bytes, entries);
|
|
633
|
+
} catch {
|
|
634
|
+
entries.length = 0;
|
|
635
|
+
}
|
|
636
|
+
const total = sum(entries);
|
|
637
|
+
if (total !== bytes.length) {
|
|
638
|
+
entries.push({ kind: "overhead", key: "unattributed", bytes: bytes.length - total });
|
|
639
|
+
}
|
|
640
|
+
this.commit(dir, entries);
|
|
641
|
+
}
|
|
642
|
+
// -------------------------------------------------------------------------
|
|
643
|
+
commit(dir, entries) {
|
|
644
|
+
for (const e of entries) {
|
|
645
|
+
if (e.bytes === 0) continue;
|
|
646
|
+
let byKey = this.rows.get(e.kind);
|
|
647
|
+
if (!byKey) {
|
|
648
|
+
byKey = /* @__PURE__ */ new Map();
|
|
649
|
+
this.rows.set(e.kind, byKey);
|
|
650
|
+
}
|
|
651
|
+
let row = byKey.get(e.key);
|
|
652
|
+
if (!row) {
|
|
653
|
+
row = { out: 0, in: 0 };
|
|
654
|
+
byKey.set(e.key, row);
|
|
655
|
+
}
|
|
656
|
+
if (dir === "in") row.in += e.bytes;
|
|
657
|
+
else row.out += e.bytes;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
describe(frame, out, options) {
|
|
661
|
+
if (frame.length === 0) return;
|
|
662
|
+
out.push({ kind: "overhead", key: "envelope", bytes: 1 });
|
|
663
|
+
const type = frame[0];
|
|
664
|
+
const payload = frame.subarray(1);
|
|
665
|
+
try {
|
|
666
|
+
switch (type) {
|
|
667
|
+
case FrameType.DELTA:
|
|
668
|
+
this.body(payload, out, "field", options?.churn, options?.spatialChurn === true);
|
|
669
|
+
return;
|
|
670
|
+
case FrameType.WRITE:
|
|
671
|
+
this.body(payload, out, "write", void 0);
|
|
672
|
+
return;
|
|
673
|
+
case FrameType.CORRECT: {
|
|
674
|
+
const r = new ByteReader3(payload);
|
|
675
|
+
this.body(r, out, "correction", void 0);
|
|
676
|
+
const tail = payload.length - r.pos;
|
|
677
|
+
if (tail > 0) out.push({ kind: "correction", key: "tail", bytes: tail });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
case FrameType.CALL: {
|
|
681
|
+
const r = new ByteReader3(payload);
|
|
682
|
+
const reqId = r.u32();
|
|
683
|
+
const rpcId = r.u16();
|
|
684
|
+
const name = this.rpcName(rpcId);
|
|
685
|
+
this.remember(options?.peer, reqId, name);
|
|
686
|
+
out.push({ kind: "rpc", key: name, bytes: payload.length });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
case FrameType.REPLY: {
|
|
690
|
+
const r = new ByteReader3(payload);
|
|
691
|
+
const reqId = r.u32();
|
|
692
|
+
out.push({ kind: "rpc", key: this.recall(options?.peer, reqId), bytes: payload.length });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
case FrameType.MSG: {
|
|
696
|
+
const kind = payload[0];
|
|
697
|
+
if (kind === 4) out.push({ kind: "voice", key: "signal", bytes: payload.length });
|
|
698
|
+
else if (kind !== void 0 && kind < MSG_KEYS.length) {
|
|
699
|
+
out.push({ kind: "message", key: MSG_KEYS[kind], bytes: payload.length });
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
case FrameType.WELCOME:
|
|
704
|
+
this.welcome(payload, out);
|
|
705
|
+
return;
|
|
706
|
+
default: {
|
|
707
|
+
const key = CONTROL_KEYS[type];
|
|
708
|
+
if (key !== void 0) out.push({ kind: "control", key, bytes: payload.length });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/** WELCOME: the snapshot inside it is `join`; everything around it is `control/welcome`. */
|
|
716
|
+
welcome(payload, out) {
|
|
717
|
+
const r = new ByteReader3(payload);
|
|
718
|
+
skipStr(r);
|
|
719
|
+
skipStr(r);
|
|
720
|
+
r.u32();
|
|
721
|
+
const len = r.varint();
|
|
722
|
+
const before = r.pos;
|
|
723
|
+
if (r.remaining < len) throw new Error("welcome: short snapshot");
|
|
724
|
+
const consumed = this.snapshotBody(payload.subarray(before, before + len), out);
|
|
725
|
+
out.push({ kind: "control", key: "welcome", bytes: payload.length - consumed });
|
|
726
|
+
}
|
|
727
|
+
snapshotBody(bytes, out) {
|
|
728
|
+
const collect = this.collector(out, "join", "join", "header", void 0);
|
|
729
|
+
return walkSnapshot(this.schema, bytes, collect);
|
|
730
|
+
}
|
|
731
|
+
body(bytes, out, fieldKind, churn, spatialChurn = false) {
|
|
732
|
+
const collect = this.collector(out, fieldKind, "overhead", "delta-header", churn, spatialChurn);
|
|
733
|
+
walkDelta(this.schema, bytes, collect);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* The walker's consumer. `op` latches: a churn op routes its own framing *and* every field
|
|
737
|
+
* that follows it into `churn`, because a synthetic add carries a whole record whose bytes are
|
|
738
|
+
* the cost of the entity crossing the boundary, not the cost of the fields changing.
|
|
739
|
+
*/
|
|
740
|
+
collector(out, fieldKind, overheadKind, headerKey, churn, spatialChurn = false) {
|
|
741
|
+
let churnCollection;
|
|
742
|
+
return {
|
|
743
|
+
header: (bytes) => {
|
|
744
|
+
out.push({ kind: overheadKind, key: headerKey, bytes });
|
|
745
|
+
},
|
|
746
|
+
op: (c, kind, id, bytes) => {
|
|
747
|
+
const isChurn = kind !== "update" && (spatialChurn ? c.visibility === "spatial-grid" : churn !== void 0 && churn.get(c.name)?.has(id) === true);
|
|
748
|
+
churnCollection = isChurn ? c.name : void 0;
|
|
749
|
+
out.push(
|
|
750
|
+
churnCollection !== void 0 ? { kind: "churn", key: c.name, bytes } : { kind: overheadKind, key: overheadKind === "join" ? headerKey : "op", bytes }
|
|
751
|
+
);
|
|
752
|
+
},
|
|
753
|
+
field: (c, f, bytes) => {
|
|
754
|
+
if (churnCollection !== void 0) {
|
|
755
|
+
out.push({ kind: "churn", key: churnCollection, bytes });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const key = `${c.name}.${f.name}`;
|
|
759
|
+
out.push({
|
|
760
|
+
kind: fieldKind === "field" && c.name === PRESENCE_COLLECTION ? "presence" : fieldKind,
|
|
761
|
+
key,
|
|
762
|
+
bytes
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
rpcName(rpcId) {
|
|
768
|
+
try {
|
|
769
|
+
return rpcByIdOf(this.rpcSchema, rpcId).name;
|
|
770
|
+
} catch {
|
|
771
|
+
return `rpc#${rpcId}`;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
remember(peer, reqId, name) {
|
|
775
|
+
if (this.pending.size >= PENDING_CALLS_MAX) this.pending.clear();
|
|
776
|
+
this.pending.set(`${peer ?? ""}\0${reqId}`, name);
|
|
777
|
+
}
|
|
778
|
+
recall(peer, reqId) {
|
|
779
|
+
const k = `${peer ?? ""}\0${reqId}`;
|
|
780
|
+
const name = this.pending.get(k);
|
|
781
|
+
if (name === void 0) return "(unmatched reply)";
|
|
782
|
+
this.pending.delete(k);
|
|
783
|
+
return name;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
function sum(entries) {
|
|
787
|
+
let n = 0;
|
|
788
|
+
for (const e of entries) n += e.bytes;
|
|
789
|
+
return n;
|
|
790
|
+
}
|
|
791
|
+
function skipStr(r) {
|
|
792
|
+
const n = r.varint();
|
|
793
|
+
if (r.remaining < n) throw new Error("unexpected end of buffer");
|
|
794
|
+
r.pos += n;
|
|
795
|
+
}
|
|
796
|
+
function diffProfiles(prev, next) {
|
|
797
|
+
const before = /* @__PURE__ */ new Map();
|
|
798
|
+
for (const r of prev.rows) before.set(`${r.kind} ${r.key}`, r);
|
|
799
|
+
const rows = [];
|
|
800
|
+
for (const r of next.rows) {
|
|
801
|
+
const b = before.get(`${r.kind} ${r.key}`);
|
|
802
|
+
const out = r.out - (b?.out ?? 0);
|
|
803
|
+
const inb = r.in - (b?.in ?? 0);
|
|
804
|
+
if (out !== 0 || inb !== 0) rows.push({ kind: r.kind, key: r.key, out, in: inb });
|
|
805
|
+
}
|
|
806
|
+
rows.sort((a, b) => b.out + b.in - (a.out + a.in) || (a.key < b.key ? -1 : 1));
|
|
807
|
+
return {
|
|
808
|
+
rows,
|
|
809
|
+
bytesIn: next.bytesIn - prev.bytesIn,
|
|
810
|
+
bytesOut: next.bytesOut - prev.bytesOut,
|
|
811
|
+
framesIn: next.framesIn - prev.framesIn,
|
|
812
|
+
framesOut: next.framesOut - prev.framesOut,
|
|
813
|
+
walks: next.walks - prev.walks
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function topProfileRows(snap, n) {
|
|
817
|
+
return [...snap.rows].sort((a, b) => b.out + b.in - (a.out + a.in)).slice(0, n);
|
|
818
|
+
}
|
|
819
|
+
function scaleProfile(snap, n) {
|
|
820
|
+
if (n <= 1) return snap;
|
|
821
|
+
return {
|
|
822
|
+
rows: snap.rows.map((r) => ({ kind: r.kind, key: r.key, out: r.out / n, in: r.in / n })),
|
|
823
|
+
bytesIn: snap.bytesIn / n,
|
|
824
|
+
bytesOut: snap.bytesOut / n,
|
|
825
|
+
framesIn: snap.framesIn / n,
|
|
826
|
+
framesOut: snap.framesOut / n,
|
|
827
|
+
walks: snap.walks
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function mergeProfiles(a, b) {
|
|
831
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const r of [...a.rows, ...b.rows]) {
|
|
833
|
+
const k = `${r.kind} ${r.key}`;
|
|
834
|
+
const row = byKey.get(k);
|
|
835
|
+
if (row) {
|
|
836
|
+
row.out += r.out;
|
|
837
|
+
row.in += r.in;
|
|
838
|
+
} else byKey.set(k, { kind: r.kind, key: r.key, out: r.out, in: r.in });
|
|
839
|
+
}
|
|
840
|
+
const rows = [...byKey.values()].sort(
|
|
841
|
+
(x, y) => y.out + y.in - (x.out + x.in) || (x.key < y.key ? -1 : 1)
|
|
842
|
+
);
|
|
843
|
+
return {
|
|
844
|
+
rows,
|
|
845
|
+
bytesIn: a.bytesIn + b.bytesIn,
|
|
846
|
+
bytesOut: a.bytesOut + b.bytesOut,
|
|
847
|
+
framesIn: a.framesIn + b.framesIn,
|
|
848
|
+
framesOut: a.framesOut + b.framesOut,
|
|
849
|
+
walks: a.walks + b.walks
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
326
853
|
// src/msg.ts
|
|
327
|
-
import { ByteReader as
|
|
854
|
+
import { ByteReader as ByteReader4, ByteWriter as ByteWriter3 } from "@irtio/schema";
|
|
328
855
|
var MSG_KIND_ALL = 0;
|
|
329
856
|
var MSG_KIND_CLIENT = 1;
|
|
330
857
|
var MSG_KIND_ROLE = 2;
|
|
331
858
|
var MSG_KIND_SERVER = 3;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
859
|
+
var MSG_KIND_VOICE = 4;
|
|
860
|
+
var MSG_KIND_TYPED = 5;
|
|
861
|
+
function isVoiceMsg(payload) {
|
|
862
|
+
return payload.length > 0 && payload[0] === MSG_KIND_VOICE;
|
|
863
|
+
}
|
|
864
|
+
function isTypedMsg(payload) {
|
|
865
|
+
return payload.length > 0 && payload[0] === MSG_KIND_TYPED;
|
|
866
|
+
}
|
|
867
|
+
function typedMsgFromClientOk(payload) {
|
|
868
|
+
if (payload.length < 4 || payload[0] !== MSG_KIND_TYPED) return false;
|
|
869
|
+
const inner = payload[3];
|
|
870
|
+
return inner === MSG_KIND_ALL || inner === MSG_KIND_CLIENT || inner === MSG_KIND_ROLE;
|
|
871
|
+
}
|
|
872
|
+
function writeTarget(w, target) {
|
|
873
|
+
switch (target.kind) {
|
|
335
874
|
case "all":
|
|
336
875
|
w.u8(MSG_KIND_ALL);
|
|
337
|
-
|
|
876
|
+
return;
|
|
338
877
|
case "client":
|
|
339
878
|
w.u8(MSG_KIND_CLIENT);
|
|
340
|
-
w.str(
|
|
341
|
-
|
|
879
|
+
w.str(target.clientId);
|
|
880
|
+
return;
|
|
342
881
|
case "role":
|
|
343
882
|
w.u8(MSG_KIND_ROLE);
|
|
344
|
-
w.str(
|
|
345
|
-
|
|
883
|
+
w.str(target.role);
|
|
884
|
+
return;
|
|
346
885
|
case "server":
|
|
347
886
|
w.u8(MSG_KIND_SERVER);
|
|
348
|
-
|
|
887
|
+
return;
|
|
888
|
+
case "voice":
|
|
889
|
+
w.u8(MSG_KIND_VOICE);
|
|
890
|
+
return;
|
|
349
891
|
}
|
|
892
|
+
}
|
|
893
|
+
function readTarget(r) {
|
|
894
|
+
const kind = r.u8();
|
|
895
|
+
switch (kind) {
|
|
896
|
+
case MSG_KIND_ALL:
|
|
897
|
+
return { kind: "all" };
|
|
898
|
+
case MSG_KIND_CLIENT:
|
|
899
|
+
return { kind: "client", clientId: r.str() };
|
|
900
|
+
case MSG_KIND_ROLE:
|
|
901
|
+
return { kind: "role", role: r.str() };
|
|
902
|
+
case MSG_KIND_SERVER:
|
|
903
|
+
return { kind: "server" };
|
|
904
|
+
case MSG_KIND_VOICE:
|
|
905
|
+
return { kind: "voice" };
|
|
906
|
+
default:
|
|
907
|
+
throw new Error(`decodeMsg: unknown target discriminator ${kind}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function encodeMsg(m) {
|
|
911
|
+
const w = new ByteWriter3();
|
|
912
|
+
if (m.typed) {
|
|
913
|
+
if (m.target.kind === "voice") {
|
|
914
|
+
throw new Error("encodeMsg: a typed message cannot address voice");
|
|
915
|
+
}
|
|
916
|
+
const index = m.typed.index;
|
|
917
|
+
if (!Number.isInteger(index) || index < 0 || index > 65535) {
|
|
918
|
+
throw new Error(`encodeMsg: typed message index out of range: ${String(index)}`);
|
|
919
|
+
}
|
|
920
|
+
w.u8(MSG_KIND_TYPED);
|
|
921
|
+
w.u16(index);
|
|
922
|
+
writeTarget(w, m.target);
|
|
923
|
+
w.bytes(m.payload);
|
|
924
|
+
return w.finish();
|
|
925
|
+
}
|
|
926
|
+
writeTarget(w, m.target);
|
|
350
927
|
w.bytes(m.payload);
|
|
351
928
|
return w.finish();
|
|
352
929
|
}
|
|
353
930
|
function decodeMsg(bytes) {
|
|
354
|
-
const r = new
|
|
931
|
+
const r = new ByteReader4(bytes);
|
|
355
932
|
const kind = r.u8();
|
|
933
|
+
if (kind === MSG_KIND_TYPED) {
|
|
934
|
+
const index = r.u16();
|
|
935
|
+
const target2 = readTarget(r);
|
|
936
|
+
if (target2.kind === "voice") {
|
|
937
|
+
throw new Error("decodeMsg: a typed message cannot address voice");
|
|
938
|
+
}
|
|
939
|
+
return { target: target2, payload: r.rest(), typed: { index } };
|
|
940
|
+
}
|
|
356
941
|
let target;
|
|
357
942
|
switch (kind) {
|
|
358
943
|
case MSG_KIND_ALL:
|
|
@@ -367,6 +952,9 @@ function decodeMsg(bytes) {
|
|
|
367
952
|
case MSG_KIND_SERVER:
|
|
368
953
|
target = { kind: "server" };
|
|
369
954
|
break;
|
|
955
|
+
case MSG_KIND_VOICE:
|
|
956
|
+
target = { kind: "voice" };
|
|
957
|
+
break;
|
|
370
958
|
default:
|
|
371
959
|
throw new Error(`decodeMsg: unknown target discriminator ${kind}`);
|
|
372
960
|
}
|
|
@@ -374,36 +962,316 @@ function decodeMsg(bytes) {
|
|
|
374
962
|
return { target, payload };
|
|
375
963
|
}
|
|
376
964
|
|
|
377
|
-
// src/
|
|
378
|
-
|
|
379
|
-
var
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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);
|
|
965
|
+
// src/voice.ts
|
|
966
|
+
var ENC = new TextEncoder();
|
|
967
|
+
var DEC = new TextDecoder();
|
|
968
|
+
function encodeVoiceMessage(m) {
|
|
969
|
+
return ENC.encode(JSON.stringify(m));
|
|
970
|
+
}
|
|
971
|
+
function decodeVoiceMessage(bytes) {
|
|
972
|
+
try {
|
|
973
|
+
const parsed = JSON.parse(DEC.decode(bytes));
|
|
974
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
975
|
+
if (typeof parsed.t !== "string") return void 0;
|
|
976
|
+
return parsed;
|
|
977
|
+
} catch {
|
|
978
|
+
return void 0;
|
|
398
979
|
}
|
|
399
|
-
return ext;
|
|
400
980
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
981
|
+
|
|
982
|
+
// src/codes.ts
|
|
983
|
+
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
984
|
+
var ROOM_TYPE_DELIMITER = ":";
|
|
985
|
+
var ROOM_TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,23}$/;
|
|
986
|
+
var ROOM_ID_RE = /^(?:[a-z0-9][a-z0-9_-]{0,23}:)?[A-Za-z0-9_-]{1,32}$/;
|
|
987
|
+
var DEFAULT_ROOM_TYPE = "default";
|
|
988
|
+
function parseRoomId(raw) {
|
|
989
|
+
if (!ROOM_ID_RE.test(raw)) return void 0;
|
|
990
|
+
const at = raw.indexOf(ROOM_TYPE_DELIMITER);
|
|
991
|
+
if (at < 0) return { type: void 0, id: raw };
|
|
992
|
+
return { type: raw.slice(0, at), id: raw.slice(at + 1) };
|
|
993
|
+
}
|
|
994
|
+
function formatRoomId(type, id) {
|
|
995
|
+
return type === void 0 ? id : `${type}${ROOM_TYPE_DELIMITER}${id}`;
|
|
996
|
+
}
|
|
997
|
+
var MATCH_CODE_LENGTH = 10;
|
|
998
|
+
|
|
999
|
+
// src/bus.ts
|
|
1000
|
+
var BUS_LIMITS = {
|
|
1001
|
+
/**
|
|
1002
|
+
* Max UTF-8 bytes in one payload, for both verbs.
|
|
1003
|
+
*
|
|
1004
|
+
* 4 KiB is a quarter of the KV value limit, and it is sized for what the feature is for: a
|
|
1005
|
+
* match-ready notice, an invite, a bracket announcement — an id, a couple of names, a reason.
|
|
1006
|
+
* A payload is also a *store* cost for `send`, because a mailbox entry persists in the alarm
|
|
1007
|
+
* sidecar, so the ceiling on one room's mailbox is this times `mailboxDepth`, and both numbers
|
|
1008
|
+
* were picked together.
|
|
1009
|
+
*/
|
|
1010
|
+
payloadBytes: 4 * 1024,
|
|
1011
|
+
/** Max UTF-8 bytes in a channel name. Names are vocabulary, not data. */
|
|
1012
|
+
channelBytes: 128,
|
|
1013
|
+
/** Max channels one room may hold a subscription to at once. */
|
|
1014
|
+
subscriptionsPerRoom: 32,
|
|
1015
|
+
/**
|
|
1016
|
+
* Max undelivered mailbox entries for one target room.
|
|
1017
|
+
*
|
|
1018
|
+
* This is a store bound as much as a memory one: entries live in the alarm sidecar, so at the
|
|
1019
|
+
* payload cap above, a full mailbox is 256 KiB of persisted object. Past this, a `send` is
|
|
1020
|
+
* refused at the sender with a named error rather than silently dropped at the target, because
|
|
1021
|
+
* a sender that is outrunning a receiver is a fact the sender can act on.
|
|
1022
|
+
*/
|
|
1023
|
+
mailboxDepth: 64,
|
|
1024
|
+
/**
|
|
1025
|
+
* How many times one mailbox entry may be delivered before it is dead-lettered.
|
|
1026
|
+
*
|
|
1027
|
+
* The bound exists because at-least-once and the room crash threshold compose badly on purpose.
|
|
1028
|
+
* A handler that always throws surfaces as a crash, and the supervisor's window is 3 restarts in
|
|
1029
|
+
* 60 seconds; an entry that re-armed forever would close the room in four deliveries, on every
|
|
1030
|
+
* wake, for as long as the entry existed. Five attempts is enough to ride out a transient (a
|
|
1031
|
+
* deploy mid-flight, a wake that raced a hibernate) and few enough that a genuinely poisonous
|
|
1032
|
+
* message costs a room one bad minute rather than its life. What happens then is a log line and
|
|
1033
|
+
* a drop: honest for v1, and the docs say so rather than implying a dead-letter queue exists.
|
|
1034
|
+
*/
|
|
1035
|
+
maxDeliveryAttempts: 5,
|
|
1036
|
+
/**
|
|
1037
|
+
* Bus operations one room may issue per second, averaged, and the burst it may take at once.
|
|
1038
|
+
*
|
|
1039
|
+
* This is the "one hot room cannot melt the supervisor" guardrail D59 asks for. It counts both
|
|
1040
|
+
* verbs together, because both cost the supervisor a fan-out or a store write, and a limiter
|
|
1041
|
+
* that only counted the cheap one would be a limiter a room could route around.
|
|
1042
|
+
*/
|
|
1043
|
+
opsPerSecond: 50,
|
|
1044
|
+
opsBurst: 100,
|
|
1045
|
+
/**
|
|
1046
|
+
* How long a delivered-but-unacknowledged mailbox entry stays armed before it is delivered
|
|
1047
|
+
* again. A visibility timeout, and it is what makes at-least-once actually true rather than
|
|
1048
|
+
* nearly true.
|
|
1049
|
+
*
|
|
1050
|
+
* Posting a message to a worker is not the same as the worker running it: a room that dies
|
|
1051
|
+
* between the post and the handler (a crash, a restart, a hibernate that raced the delivery)
|
|
1052
|
+
* would otherwise lose the message with no trace, which is at-most-once wearing at-least-once's
|
|
1053
|
+
* name. So an entry is re-armed for this long at the moment it is posted, and cleared only when
|
|
1054
|
+
* the worker says the handler ran. 30 seconds is far longer than a handler takes and short
|
|
1055
|
+
* enough that a genuinely lost delivery is retried while anyone still cares.
|
|
1056
|
+
*/
|
|
1057
|
+
redeliveryDelayMs: 3e4
|
|
1058
|
+
};
|
|
1059
|
+
var BUS_CHANNEL_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
1060
|
+
var BUS_ERRORS = {
|
|
1061
|
+
badChannel: "E_BUS_BAD_CHANNEL",
|
|
1062
|
+
payloadTooLarge: "E_BUS_PAYLOAD_TOO_LARGE",
|
|
1063
|
+
tooManySubscriptions: "E_BUS_TOO_MANY_SUBSCRIPTIONS",
|
|
1064
|
+
/** The named room does not exist in this tenant, or is a relay room and runs no handlers. */
|
|
1065
|
+
noSuchRoom: "E_BUS_NO_SUCH_ROOM",
|
|
1066
|
+
mailboxFull: "E_BUS_MAILBOX_FULL",
|
|
1067
|
+
rateLimited: "E_BUS_RATE_LIMITED"
|
|
1068
|
+
};
|
|
1069
|
+
function utf8Bytes(s) {
|
|
1070
|
+
return new TextEncoder().encode(s).length;
|
|
1071
|
+
}
|
|
1072
|
+
function busChannelProblem(channel) {
|
|
1073
|
+
if (typeof channel !== "string" || channel === "") {
|
|
1074
|
+
return { code: BUS_ERRORS.badChannel, message: "a channel name is required" };
|
|
1075
|
+
}
|
|
1076
|
+
if (utf8Bytes(channel) > BUS_LIMITS.channelBytes) {
|
|
1077
|
+
return {
|
|
1078
|
+
code: BUS_ERRORS.badChannel,
|
|
1079
|
+
message: `channel name is longer than ${BUS_LIMITS.channelBytes} bytes`
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
if (!BUS_CHANNEL_RE.test(channel)) {
|
|
1083
|
+
return {
|
|
1084
|
+
code: BUS_ERRORS.badChannel,
|
|
1085
|
+
message: `illegal channel name ${JSON.stringify(channel)}: lowercase, starts alphanumeric, and made of letters, digits, dots, underscores and dashes`
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return void 0;
|
|
1089
|
+
}
|
|
1090
|
+
function busPayloadProblem(payload) {
|
|
1091
|
+
if (typeof payload !== "string") {
|
|
1092
|
+
return {
|
|
1093
|
+
code: BUS_ERRORS.payloadTooLarge,
|
|
1094
|
+
message: "a bus payload must be a string (serialise your own object)"
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const bytes = utf8Bytes(payload);
|
|
1098
|
+
if (bytes > BUS_LIMITS.payloadBytes) {
|
|
1099
|
+
return {
|
|
1100
|
+
code: BUS_ERRORS.payloadTooLarge,
|
|
1101
|
+
message: `payload is ${bytes} bytes; the limit is ${BUS_LIMITS.payloadBytes}`
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
return void 0;
|
|
1105
|
+
}
|
|
1106
|
+
var BUS_MAILBOX_PREFIX = "__bus.";
|
|
1107
|
+
function isMailboxAlarm(name) {
|
|
1108
|
+
return name.startsWith(BUS_MAILBOX_PREFIX);
|
|
1109
|
+
}
|
|
1110
|
+
var BUS_OUTBOX_PREFIX = "__busout.";
|
|
1111
|
+
function isOutboxAlarm(name) {
|
|
1112
|
+
return name.startsWith(BUS_OUTBOX_PREFIX);
|
|
1113
|
+
}
|
|
1114
|
+
var BUS_OUTBOX = {
|
|
1115
|
+
firstRetryMs: 1e3,
|
|
1116
|
+
maxRetryMs: 6e4,
|
|
1117
|
+
ttlMs: 60 * 60 * 1e3
|
|
1118
|
+
};
|
|
1119
|
+
var BUS_SHARD_STARTING = "E_SHARD_STARTING";
|
|
1120
|
+
|
|
1121
|
+
// src/classes.ts
|
|
1122
|
+
var ROOM_MEMORY_MB_MIN = 32;
|
|
1123
|
+
var ROOM_MEMORY_MB_MAX = 1024;
|
|
1124
|
+
var ROOM_CLASS_MAX_MB = {
|
|
1125
|
+
small: 64,
|
|
1126
|
+
medium: 256,
|
|
1127
|
+
large: ROOM_MEMORY_MB_MAX
|
|
1128
|
+
};
|
|
1129
|
+
function roomClassFor(memoryMb) {
|
|
1130
|
+
if (memoryMb === void 0 || !Number.isFinite(memoryMb)) return "small";
|
|
1131
|
+
if (memoryMb <= ROOM_CLASS_MAX_MB.small) return "small";
|
|
1132
|
+
if (memoryMb <= ROOM_CLASS_MAX_MB.medium) return "medium";
|
|
1133
|
+
return "large";
|
|
1134
|
+
}
|
|
1135
|
+
function roomHoursSourceFor(cls) {
|
|
1136
|
+
return cls === "small" ? "room" : cls;
|
|
1137
|
+
}
|
|
1138
|
+
function roomClassOfSource(source) {
|
|
1139
|
+
if (source === "room") return "small";
|
|
1140
|
+
if (source === "medium" || source === "large") return source;
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/sizing.ts
|
|
1145
|
+
var VM_MEM_MIB_MIN = 128;
|
|
1146
|
+
var VM_MEM_MIB_MAX = 4096;
|
|
1147
|
+
var SUPERVISOR_BASELINE_MIB = 54;
|
|
1148
|
+
var NATIVE_RESERVE_FRACTION = 8;
|
|
1149
|
+
var PHYSICS_HEADROOM_MIB = 112;
|
|
1150
|
+
var YOUNG_GEN_MB = 32;
|
|
1151
|
+
var DEFAULT_MEMORY_MB = 32;
|
|
1152
|
+
var DEFAULT_MAX_AWAKE = 1;
|
|
1153
|
+
var MAX_AWAKE_MAX = 256;
|
|
1154
|
+
function nativeReserveOf(vmMib) {
|
|
1155
|
+
return Math.floor(vmMib / NATIVE_RESERVE_FRACTION);
|
|
1156
|
+
}
|
|
1157
|
+
function typeFootprintMib(type) {
|
|
1158
|
+
const memoryMb = normalizeMemoryMb(type.memoryMb);
|
|
1159
|
+
const maxAwake = normalizeMaxAwake(type.maxAwake);
|
|
1160
|
+
return (memoryMb + YOUNG_GEN_MB) * maxAwake;
|
|
1161
|
+
}
|
|
1162
|
+
function normalizeMemoryMb(value) {
|
|
1163
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) return DEFAULT_MEMORY_MB;
|
|
1164
|
+
return Math.floor(value);
|
|
1165
|
+
}
|
|
1166
|
+
function normalizeMaxAwake(value) {
|
|
1167
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) return DEFAULT_MAX_AWAKE;
|
|
1168
|
+
return Math.min(MAX_AWAKE_MAX, Math.floor(value));
|
|
1169
|
+
}
|
|
1170
|
+
function vmSizeFor(types, options = {}) {
|
|
1171
|
+
const baselineMib = options.baselineMib ?? SUPERVISOR_BASELINE_MIB;
|
|
1172
|
+
let workersMib = 0;
|
|
1173
|
+
let anyPhysics = false;
|
|
1174
|
+
for (const type of types) {
|
|
1175
|
+
workersMib += typeFootprintMib(type);
|
|
1176
|
+
if (type.physics === true) anyPhysics = true;
|
|
1177
|
+
}
|
|
1178
|
+
const physicsHeadroomMib = anyPhysics ? PHYSICS_HEADROOM_MIB : 0;
|
|
1179
|
+
const preReserveMib = workersMib + baselineMib + physicsHeadroomMib;
|
|
1180
|
+
const unclamped = Math.ceil(
|
|
1181
|
+
preReserveMib * NATIVE_RESERVE_FRACTION / (NATIVE_RESERVE_FRACTION - 1)
|
|
1182
|
+
);
|
|
1183
|
+
const vmMib = Math.min(VM_MEM_MIB_MAX, Math.max(VM_MEM_MIB_MIN, unclamped));
|
|
1184
|
+
const clamped = unclamped < VM_MEM_MIB_MIN ? "floor" : unclamped > VM_MEM_MIB_MAX ? "ceiling" : void 0;
|
|
1185
|
+
const parts = types.map(
|
|
1186
|
+
(t) => `${t.type} ${normalizeMemoryMb(t.memoryMb)}+${YOUNG_GEN_MB} MB x ${normalizeMaxAwake(t.maxAwake)}`
|
|
1187
|
+
).join(", ");
|
|
1188
|
+
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)` : "");
|
|
1189
|
+
return {
|
|
1190
|
+
vmMib,
|
|
1191
|
+
workersMib,
|
|
1192
|
+
baselineMib,
|
|
1193
|
+
physicsHeadroomMib,
|
|
1194
|
+
preReserveMib,
|
|
1195
|
+
nativeReserveMib: vmMib - preReserveMib,
|
|
1196
|
+
...clamped !== void 0 ? { clamped } : {},
|
|
1197
|
+
detail
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
var ROOMS_PER_VCPU = {
|
|
1201
|
+
small: 20,
|
|
1202
|
+
medium: 8,
|
|
1203
|
+
large: 5
|
|
1204
|
+
};
|
|
1205
|
+
var CPU_QUOTA_PCT_MIN = 10;
|
|
1206
|
+
var CPU_QUOTA_PCT_MAX = 100;
|
|
1207
|
+
function vmCpuFor(types) {
|
|
1208
|
+
if (types.length === 0) {
|
|
1209
|
+
return {
|
|
1210
|
+
quotaPct: CPU_QUOTA_PCT_MAX,
|
|
1211
|
+
rawPct: CPU_QUOTA_PCT_MAX,
|
|
1212
|
+
undeclared: true,
|
|
1213
|
+
detail: "no room types declared, so no CPU throttle"
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const declared = types.some(
|
|
1217
|
+
(t) => t.memoryMb !== void 0 || t.maxAwake !== void 0 && t.maxAwake > 0
|
|
1218
|
+
);
|
|
1219
|
+
if (!declared) {
|
|
1220
|
+
return {
|
|
1221
|
+
quotaPct: CPU_QUOTA_PCT_MAX,
|
|
1222
|
+
rawPct: CPU_QUOTA_PCT_MAX,
|
|
1223
|
+
undeclared: true,
|
|
1224
|
+
detail: `${types.length} room type(s), none declaring memoryMb or maxAwake, so no CPU throttle`
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
let rawPct = 0;
|
|
1228
|
+
const parts = [];
|
|
1229
|
+
for (const type of types) {
|
|
1230
|
+
const cls = roomClassFor(normalizeMemoryMbForClass(type.memoryMb));
|
|
1231
|
+
const maxAwake = normalizeMaxAwake(type.maxAwake);
|
|
1232
|
+
const density = ROOMS_PER_VCPU[cls];
|
|
1233
|
+
const share = maxAwake * CPU_QUOTA_PCT_MAX / density;
|
|
1234
|
+
rawPct += share;
|
|
1235
|
+
parts.push(`${type.type} ${cls} x ${maxAwake} / ${density} per vCPU`);
|
|
1236
|
+
}
|
|
1237
|
+
const rounded = Math.ceil(rawPct);
|
|
1238
|
+
const quotaPct = Math.min(CPU_QUOTA_PCT_MAX, Math.max(CPU_QUOTA_PCT_MIN, rounded));
|
|
1239
|
+
const clamped = rounded < CPU_QUOTA_PCT_MIN ? "floor" : rounded > CPU_QUOTA_PCT_MAX ? "ceiling" : void 0;
|
|
1240
|
+
return {
|
|
1241
|
+
quotaPct,
|
|
1242
|
+
rawPct,
|
|
1243
|
+
undeclared: false,
|
|
1244
|
+
...clamped !== void 0 ? { clamped } : {},
|
|
1245
|
+
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}%)` : "")
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function normalizeMemoryMbForClass(value) {
|
|
1249
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) return void 0;
|
|
1250
|
+
return Math.floor(value);
|
|
1251
|
+
}
|
|
1252
|
+
function declarationsFit(vmMemMib, types, options = {}) {
|
|
1253
|
+
const required = vmSizeFor(types, options);
|
|
1254
|
+
return { fits: required.vmMib <= vmMemMib, required };
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/retention.ts
|
|
1258
|
+
var RETENTION_RE = /^[1-9][0-9]{0,4}(m|h|d)$/;
|
|
1259
|
+
var RETENTION_MIN_MS = 6e4;
|
|
1260
|
+
var RETENTION_MAX_MS = 3650 * 24 * 60 * 60 * 1e3;
|
|
1261
|
+
var UNIT_MS = {
|
|
1262
|
+
m: 6e4,
|
|
1263
|
+
h: 60 * 60 * 1e3,
|
|
1264
|
+
d: 24 * 60 * 60 * 1e3
|
|
1265
|
+
};
|
|
1266
|
+
function parseRetention(raw) {
|
|
1267
|
+
if (typeof raw !== "string") return void 0;
|
|
1268
|
+
if (!RETENTION_RE.test(raw)) return void 0;
|
|
1269
|
+
const unit = raw.slice(-1);
|
|
1270
|
+
const scale = UNIT_MS[unit];
|
|
1271
|
+
if (scale === void 0) return void 0;
|
|
1272
|
+
const ms = Number(raw.slice(0, -1)) * scale;
|
|
1273
|
+
if (ms < RETENTION_MIN_MS || ms > RETENTION_MAX_MS) return void 0;
|
|
1274
|
+
return ms;
|
|
407
1275
|
}
|
|
408
1276
|
|
|
409
1277
|
// src/origin.ts
|
|
@@ -429,14 +1297,56 @@ function parseOriginList(raw) {
|
|
|
429
1297
|
return raw.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
|
|
430
1298
|
}
|
|
431
1299
|
export {
|
|
1300
|
+
BUS_CHANNEL_RE,
|
|
1301
|
+
BUS_ERRORS,
|
|
1302
|
+
BUS_LIMITS,
|
|
1303
|
+
BUS_MAILBOX_PREFIX,
|
|
1304
|
+
BUS_OUTBOX,
|
|
1305
|
+
BUS_OUTBOX_PREFIX,
|
|
1306
|
+
BUS_SHARD_STARTING,
|
|
1307
|
+
CLOSE_EGRESS_WALL,
|
|
1308
|
+
CLOSE_ROOM_DELETED,
|
|
1309
|
+
CLOSE_TRY_AGAIN_LATER,
|
|
1310
|
+
CODE_ALPHABET,
|
|
1311
|
+
CPU_QUOTA_PCT_MAX,
|
|
1312
|
+
CPU_QUOTA_PCT_MIN,
|
|
1313
|
+
DEFAULT_MAX_AWAKE,
|
|
1314
|
+
DEFAULT_MEMORY_MB,
|
|
1315
|
+
DEFAULT_ROOM_TYPE,
|
|
1316
|
+
EMPTY_PROFILE,
|
|
432
1317
|
ERROR_CATALOGUE,
|
|
433
1318
|
ErrorCode,
|
|
434
1319
|
FrameType,
|
|
1320
|
+
HELLO_SCHEMA_SWAP_BIT,
|
|
1321
|
+
MATCH_CODE_LENGTH,
|
|
1322
|
+
MAX_AWAKE_MAX,
|
|
1323
|
+
MSG_KIND_TYPED,
|
|
1324
|
+
NATIVE_RESERVE_FRACTION,
|
|
1325
|
+
PHYSICS_HEADROOM_MIB,
|
|
435
1326
|
PRESENCE_COLLECTION,
|
|
1327
|
+
PROFILE_KINDS,
|
|
436
1328
|
PROTOCOL_VERSION,
|
|
1329
|
+
ProfileLedger,
|
|
437
1330
|
RELAY_HASH8,
|
|
1331
|
+
RETENTION_MAX_MS,
|
|
1332
|
+
RETENTION_MIN_MS,
|
|
1333
|
+
RETENTION_RE,
|
|
1334
|
+
ROOMS_PER_VCPU,
|
|
1335
|
+
ROOM_CLASS_MAX_MB,
|
|
1336
|
+
ROOM_ID_RE,
|
|
1337
|
+
ROOM_MEMORY_MB_MAX,
|
|
1338
|
+
ROOM_MEMORY_MB_MIN,
|
|
1339
|
+
ROOM_TYPE_DELIMITER,
|
|
1340
|
+
ROOM_TYPE_RE,
|
|
1341
|
+
SUPERVISOR_BASELINE_MIB,
|
|
1342
|
+
VM_MEM_MIB_MAX,
|
|
1343
|
+
VM_MEM_MIB_MIN,
|
|
1344
|
+
YOUNG_GEN_MB,
|
|
438
1345
|
builtinRpcs,
|
|
1346
|
+
busChannelProblem,
|
|
1347
|
+
busPayloadProblem,
|
|
439
1348
|
correctPayload,
|
|
1349
|
+
declarationsFit,
|
|
440
1350
|
decodeCall,
|
|
441
1351
|
decodeErrorPayload,
|
|
442
1352
|
decodeFrame,
|
|
@@ -445,8 +1355,10 @@ export {
|
|
|
445
1355
|
decodePing,
|
|
446
1356
|
decodePong,
|
|
447
1357
|
decodeReply,
|
|
1358
|
+
decodeVoiceMessage,
|
|
448
1359
|
decodeWelcome,
|
|
449
1360
|
deltaPayload,
|
|
1361
|
+
diffProfiles,
|
|
450
1362
|
encodeCall,
|
|
451
1363
|
encodeCorrectFrame,
|
|
452
1364
|
encodeDeltaFrame,
|
|
@@ -457,23 +1369,45 @@ export {
|
|
|
457
1369
|
encodePing,
|
|
458
1370
|
encodePong,
|
|
459
1371
|
encodeReply,
|
|
1372
|
+
encodeSchemaFrame,
|
|
1373
|
+
encodeVoiceMessage,
|
|
460
1374
|
encodeWelcome,
|
|
461
1375
|
encodeWriteFrame,
|
|
462
1376
|
errorByCode,
|
|
463
1377
|
formatError,
|
|
1378
|
+
formatRoomId,
|
|
464
1379
|
isFrameType,
|
|
465
1380
|
isLocalhostOrigin,
|
|
1381
|
+
isMailboxAlarm,
|
|
1382
|
+
isOutboxAlarm,
|
|
466
1383
|
isRelayHash8,
|
|
1384
|
+
isTypedMsg,
|
|
1385
|
+
isVoiceMsg,
|
|
1386
|
+
mergeProfiles,
|
|
1387
|
+
nativeReserveOf,
|
|
467
1388
|
originAllowed,
|
|
468
1389
|
parseOriginList,
|
|
1390
|
+
parseRetention,
|
|
1391
|
+
parseRoomId,
|
|
469
1392
|
presenceEntity,
|
|
470
1393
|
readCorrectAppliedTick,
|
|
471
1394
|
readCorrectClientTick,
|
|
1395
|
+
readSchemaPayload,
|
|
472
1396
|
relaySchema,
|
|
473
1397
|
requestOwnership,
|
|
1398
|
+
roomClassFor,
|
|
1399
|
+
roomClassOfSource,
|
|
1400
|
+
roomHoursSourceFor,
|
|
474
1401
|
rpcByIdOf,
|
|
475
1402
|
rpcIdOf,
|
|
476
1403
|
rpcTable,
|
|
1404
|
+
scaleProfile,
|
|
1405
|
+
schemaPayload,
|
|
1406
|
+
topProfileRows,
|
|
1407
|
+
typeFootprintMib,
|
|
1408
|
+
typedMsgFromClientOk,
|
|
1409
|
+
vmCpuFor,
|
|
1410
|
+
vmSizeFor,
|
|
477
1411
|
withBuiltins,
|
|
478
1412
|
writePayload
|
|
479
1413
|
};
|