@irtio/runtime 0.10.0 → 0.11.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/{chunk-MTBMGUGG.js → chunk-ACC3SDIO.js} +1 -1
- package/dist/{chunk-KGZEQTJR.js → chunk-GYPUNTQN.js} +1001 -94
- package/dist/{contract-C5aqs49-.d.ts → contract-Hk4SkDtW.d.ts} +101 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +12 -2
- package/dist/{room-Db3iPBKM.d.ts → room-0FZBGyke.d.ts} +9 -1
- package/dist/test/index.d.ts +26 -38
- package/dist/test/index.js +60 -4
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +2 -2
- package/package.json +4 -4
|
@@ -9,6 +9,181 @@ var RoomFullError = class extends Error {
|
|
|
9
9
|
name = "RoomFullError";
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/core/caps.ts
|
|
13
|
+
import {
|
|
14
|
+
ErrorCode,
|
|
15
|
+
FrameType,
|
|
16
|
+
encodeErrorPayload,
|
|
17
|
+
encodeFrame,
|
|
18
|
+
formatError
|
|
19
|
+
} from "@irtio/protocol";
|
|
20
|
+
import { encodeSnapshot } from "@irtio/schema";
|
|
21
|
+
var RELAY_STATE_BYTES = 512 * 1024;
|
|
22
|
+
var RELAY_RECORDS_PER_COLLECTION = 1024;
|
|
23
|
+
var RELAY_WRITES_PER_SECOND = 35;
|
|
24
|
+
var DEFAULT_RECORDS_PER_COLLECTION = RELAY_RECORDS_PER_COLLECTION;
|
|
25
|
+
var RELAY_WRITE_CAPS = {
|
|
26
|
+
stateBytes: RELAY_STATE_BYTES,
|
|
27
|
+
recordsPerCollection: RELAY_RECORDS_PER_COLLECTION,
|
|
28
|
+
writesPerSecond: RELAY_WRITES_PER_SECOND
|
|
29
|
+
};
|
|
30
|
+
function refuseByName(core, clientId, code, vars) {
|
|
31
|
+
const message = formatError(code, vars);
|
|
32
|
+
core.recordEvent("write-rejected", clientId, `${code}: ${message}`);
|
|
33
|
+
core.send(
|
|
34
|
+
clientId,
|
|
35
|
+
encodeFrame(
|
|
36
|
+
FrameType.ERROR,
|
|
37
|
+
encodeErrorPayload({ code: ErrorCode[code].code, message, fatal: false })
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function maxTypeBytes(t) {
|
|
42
|
+
const optional = t.opt ? 1 : 0;
|
|
43
|
+
switch (t.kind) {
|
|
44
|
+
case "bool":
|
|
45
|
+
case "u8":
|
|
46
|
+
return optional + 1;
|
|
47
|
+
case "u16":
|
|
48
|
+
return optional + 2;
|
|
49
|
+
case "u32":
|
|
50
|
+
case "i32":
|
|
51
|
+
case "f32":
|
|
52
|
+
return optional + 4;
|
|
53
|
+
case "f64":
|
|
54
|
+
return optional + 8;
|
|
55
|
+
case "enum":
|
|
56
|
+
return optional + 2;
|
|
57
|
+
case "str":
|
|
58
|
+
return optional + 5 + t.max * 4;
|
|
59
|
+
case "ref":
|
|
60
|
+
return optional + 5 + 64 * 4;
|
|
61
|
+
case "list":
|
|
62
|
+
return optional + 5 + t.max * maxTypeBytes(t.item);
|
|
63
|
+
case "struct": {
|
|
64
|
+
let sum = optional;
|
|
65
|
+
for (const f of Object.values(t.fields)) sum += maxTypeBytes(f);
|
|
66
|
+
return sum;
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
return optional + 8;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function maxRecordBytes(c) {
|
|
73
|
+
let sum = 5 + 64 * 4 + 5 + 64 * 4;
|
|
74
|
+
for (const f of c.fields) sum += maxTypeBytes(f.type);
|
|
75
|
+
return sum;
|
|
76
|
+
}
|
|
77
|
+
var STALE_MEASURE_TICKS = 300;
|
|
78
|
+
var StateMeter = class {
|
|
79
|
+
constructor(core, cap) {
|
|
80
|
+
this.core = core;
|
|
81
|
+
this.cap = cap;
|
|
82
|
+
this.figure = this.measure();
|
|
83
|
+
}
|
|
84
|
+
core;
|
|
85
|
+
cap;
|
|
86
|
+
/** The running figure: the last exact measure plus every accepted write's upper bound since. */
|
|
87
|
+
figure;
|
|
88
|
+
measuredAtTick = -1;
|
|
89
|
+
measure() {
|
|
90
|
+
const bytes = encodeSnapshot(this.core.ext, this.core.plain, { tick: this.core.tick });
|
|
91
|
+
this.measuredAtTick = this.core.tick;
|
|
92
|
+
return bytes.length;
|
|
93
|
+
}
|
|
94
|
+
/** Has the figure gone long enough without an exact measure to be worth distrusting? */
|
|
95
|
+
stale() {
|
|
96
|
+
return this.core.tick - this.measuredAtTick >= STALE_MEASURE_TICKS;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The truth, if it is worth paying for: over half the cap, or stale, and at most once a tick.
|
|
100
|
+
* A small room that is being written to steadily encodes once every {@link STALE_MEASURE_TICKS}
|
|
101
|
+
* ticks and otherwise uses the running figure, which is why it costs next to nothing.
|
|
102
|
+
*/
|
|
103
|
+
refresh() {
|
|
104
|
+
if (this.measuredAtTick === this.core.tick) return;
|
|
105
|
+
if (this.figure * 2 <= this.cap && !this.stale()) return;
|
|
106
|
+
this.figure = this.measure();
|
|
107
|
+
}
|
|
108
|
+
/** `true` when a write that could add `growth` bytes must be refused. */
|
|
109
|
+
wouldExceed(growth) {
|
|
110
|
+
if (this.stale()) this.refresh();
|
|
111
|
+
if (this.figure + growth <= this.cap) return false;
|
|
112
|
+
this.refresh();
|
|
113
|
+
return this.figure + growth > this.cap;
|
|
114
|
+
}
|
|
115
|
+
/** Records an accepted write's upper bound. */
|
|
116
|
+
grew(growth) {
|
|
117
|
+
this.figure += growth;
|
|
118
|
+
}
|
|
119
|
+
/** The current figure, for the room row and `irtio rooms`. Exact when it matters. */
|
|
120
|
+
bytes() {
|
|
121
|
+
this.refresh();
|
|
122
|
+
return this.figure;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var states = /* @__PURE__ */ new WeakMap();
|
|
126
|
+
function installCaps(core, caps) {
|
|
127
|
+
states.set(core, {
|
|
128
|
+
caps,
|
|
129
|
+
meter: caps.stateBytes !== void 0 ? new StateMeter(core, caps.stateBytes) : void 0,
|
|
130
|
+
buckets: /* @__PURE__ */ new Map()
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function stateOf(core) {
|
|
134
|
+
let s = states.get(core);
|
|
135
|
+
if (!s) {
|
|
136
|
+
s = { caps: {}, meter: void 0, buckets: /* @__PURE__ */ new Map() };
|
|
137
|
+
states.set(core, s);
|
|
138
|
+
}
|
|
139
|
+
return s;
|
|
140
|
+
}
|
|
141
|
+
function recordCap(core) {
|
|
142
|
+
return stateOf(core).caps.recordsPerCollection ?? DEFAULT_RECORDS_PER_COLLECTION;
|
|
143
|
+
}
|
|
144
|
+
function admitGrowth(core, clientId, growth) {
|
|
145
|
+
const meter = stateOf(core).meter;
|
|
146
|
+
if (!meter) return true;
|
|
147
|
+
if (meter.wouldExceed(growth)) {
|
|
148
|
+
refuseByName(core, clientId, "E_STATE_CAP", { limit: meter.cap });
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
meter.grew(growth);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
function stateBytesOf(core) {
|
|
155
|
+
return stateOf(core).meter?.bytes();
|
|
156
|
+
}
|
|
157
|
+
function admitWrite(core, clientId, nowMs, ops = 1) {
|
|
158
|
+
const s = stateOf(core);
|
|
159
|
+
const limit = s.caps.writesPerSecond;
|
|
160
|
+
if (limit === void 0) return true;
|
|
161
|
+
const cost = Math.max(1, ops);
|
|
162
|
+
let bucket = s.buckets.get(clientId);
|
|
163
|
+
if (!bucket) {
|
|
164
|
+
bucket = { tokens: limit, at: nowMs, told: false };
|
|
165
|
+
s.buckets.set(clientId, bucket);
|
|
166
|
+
}
|
|
167
|
+
const elapsed = nowMs - bucket.at;
|
|
168
|
+
if (elapsed > 0) {
|
|
169
|
+
bucket.tokens = Math.min(limit, bucket.tokens + elapsed * limit / 1e3);
|
|
170
|
+
bucket.at = nowMs;
|
|
171
|
+
}
|
|
172
|
+
if (bucket.tokens >= cost) {
|
|
173
|
+
bucket.tokens -= cost;
|
|
174
|
+
bucket.told = false;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (!bucket.told) {
|
|
178
|
+
bucket.told = true;
|
|
179
|
+
refuseByName(core, clientId, "E_WRITE_RATE", { limit });
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function forgetWriter(core, clientId) {
|
|
184
|
+
states.get(core)?.buckets.delete(clientId);
|
|
185
|
+
}
|
|
186
|
+
|
|
12
187
|
// src/core/inspect.ts
|
|
13
188
|
import { EntityCollection } from "@irtio/schema";
|
|
14
189
|
var EVENT_RING_SIZE = 100;
|
|
@@ -39,17 +214,17 @@ function inspectState(schema, plain) {
|
|
|
39
214
|
|
|
40
215
|
// src/core/rpc.ts
|
|
41
216
|
import {
|
|
42
|
-
FrameType,
|
|
217
|
+
FrameType as FrameType2,
|
|
43
218
|
decodeCall,
|
|
44
219
|
decodeReply,
|
|
45
220
|
encodeCall,
|
|
46
|
-
encodeFrame,
|
|
221
|
+
encodeFrame as encodeFrame2,
|
|
47
222
|
encodeReply,
|
|
48
223
|
rpcByIdOf,
|
|
49
224
|
rpcTable
|
|
50
225
|
} from "@irtio/protocol";
|
|
51
226
|
import {
|
|
52
|
-
SERVER_OWNER,
|
|
227
|
+
SERVER_OWNER as SERVER_OWNER2,
|
|
53
228
|
decodeFields,
|
|
54
229
|
encodeFields,
|
|
55
230
|
validateValue
|
|
@@ -81,15 +256,251 @@ function deepEqual(a, b) {
|
|
|
81
256
|
return true;
|
|
82
257
|
}
|
|
83
258
|
|
|
259
|
+
// src/core/ownership.ts
|
|
260
|
+
import { SERVER_OWNER, defaultRecord } from "@irtio/schema";
|
|
261
|
+
var states2 = /* @__PURE__ */ new WeakMap();
|
|
262
|
+
function stateOf2(core) {
|
|
263
|
+
let s = states2.get(core);
|
|
264
|
+
if (!s) {
|
|
265
|
+
s = { holds: /* @__PURE__ */ new Map(), askWarned: false };
|
|
266
|
+
states2.set(core, s);
|
|
267
|
+
}
|
|
268
|
+
return s;
|
|
269
|
+
}
|
|
270
|
+
function keyOf(collection, id) {
|
|
271
|
+
return `${collection} ${id}`;
|
|
272
|
+
}
|
|
273
|
+
function policyCollections(core) {
|
|
274
|
+
return core.ext.collections.filter(
|
|
275
|
+
(c) => c.kind === "entity" && c.ownership !== void 0
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
function wallNow(core) {
|
|
279
|
+
return core.wallNow();
|
|
280
|
+
}
|
|
281
|
+
function holdOn(core, collection, id) {
|
|
282
|
+
return states2.get(core)?.holds.get(keyOf(collection, id));
|
|
283
|
+
}
|
|
284
|
+
function holdCount(core) {
|
|
285
|
+
return states2.get(core)?.holds.size ?? 0;
|
|
286
|
+
}
|
|
287
|
+
function placeHolds(core, clientId, playerId) {
|
|
288
|
+
const s = stateOf2(core);
|
|
289
|
+
const now = wallNow(core);
|
|
290
|
+
for (const c of policyCollections(core)) {
|
|
291
|
+
const policy = c.ownership;
|
|
292
|
+
const coll = plainEntity(core.plain, c.name);
|
|
293
|
+
for (const id of [...coll.ids()]) {
|
|
294
|
+
if (coll.ownerOf(id) !== clientId) continue;
|
|
295
|
+
const hold = {
|
|
296
|
+
collection: c.name,
|
|
297
|
+
id,
|
|
298
|
+
holder: playerId,
|
|
299
|
+
deadlineMs: now + policy.holdMs,
|
|
300
|
+
// `then` is the D77 spec's own name for the leave action; a hold is plain data nothing
|
|
301
|
+
// ever awaits.
|
|
302
|
+
// biome-ignore lint/suspicious/noThenProperty: see above
|
|
303
|
+
then: policy.then
|
|
304
|
+
};
|
|
305
|
+
if (policy.holdMs <= 0) {
|
|
306
|
+
act(core, hold);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
s.holds.set(keyOf(c.name, id), hold);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function reclaimHolds(core, clientId, playerId) {
|
|
314
|
+
const s = states2.get(core);
|
|
315
|
+
if (!s || s.holds.size === 0) return 0;
|
|
316
|
+
let reclaimed = 0;
|
|
317
|
+
for (const [key, hold] of [...s.holds]) {
|
|
318
|
+
if (hold.holder !== playerId) continue;
|
|
319
|
+
s.holds.delete(key);
|
|
320
|
+
const coll = plainEntity(core.plain, hold.collection);
|
|
321
|
+
if (!coll.has(hold.id)) continue;
|
|
322
|
+
trackedEntity(core, hold.collection).setOwner(hold.id, clientId);
|
|
323
|
+
reclaimed++;
|
|
324
|
+
}
|
|
325
|
+
return reclaimed;
|
|
326
|
+
}
|
|
327
|
+
function expireHolds(core) {
|
|
328
|
+
const s = states2.get(core);
|
|
329
|
+
if (!s || s.holds.size === 0) return;
|
|
330
|
+
const now = wallNow(core);
|
|
331
|
+
for (const [key, hold] of [...s.holds]) {
|
|
332
|
+
if (hold.deadlineMs > now) continue;
|
|
333
|
+
s.holds.delete(key);
|
|
334
|
+
act(core, hold);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function act(core, hold) {
|
|
338
|
+
const coll = plainEntity(core.plain, hold.collection);
|
|
339
|
+
if (!coll.has(hold.id)) return;
|
|
340
|
+
const tracked = trackedEntity(core, hold.collection);
|
|
341
|
+
switch (hold.then) {
|
|
342
|
+
case "remove":
|
|
343
|
+
tracked.remove(hold.id);
|
|
344
|
+
return;
|
|
345
|
+
case "release":
|
|
346
|
+
tracked.setOwner(hold.id, SERVER_OWNER);
|
|
347
|
+
return;
|
|
348
|
+
case "nearest": {
|
|
349
|
+
const to = nearestConnected(core, hold.collection, hold.id);
|
|
350
|
+
tracked.setOwner(hold.id, to ?? SERVER_OWNER);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
case "random": {
|
|
354
|
+
const candidates = connectedClients(core);
|
|
355
|
+
if (candidates.length === 0) {
|
|
356
|
+
tracked.setOwner(hold.id, SERVER_OWNER);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const pick = candidates[Math.floor(core.rng.next() * candidates.length)];
|
|
360
|
+
tracked.setOwner(hold.id, pick);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
default:
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function connectedClients(core) {
|
|
368
|
+
const out = [];
|
|
369
|
+
for (const entry of core.clients.values()) if (entry.connected) out.push(entry.clientId);
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
function numberAt(record, field) {
|
|
373
|
+
if (!record) return void 0;
|
|
374
|
+
const v = record[field];
|
|
375
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
376
|
+
}
|
|
377
|
+
function distanceTo(core, policy, object, clientId) {
|
|
378
|
+
const position = policy.position;
|
|
379
|
+
if (!position || !object) return void 0;
|
|
380
|
+
const players = plainEntity(core.plain, position.players);
|
|
381
|
+
const player = players.get(clientId);
|
|
382
|
+
const px = numberAt(player, position.x);
|
|
383
|
+
const py = numberAt(player, position.y);
|
|
384
|
+
const ox = numberAt(object, position.x);
|
|
385
|
+
const oy = numberAt(object, position.y);
|
|
386
|
+
if (px === void 0 || py === void 0 || ox === void 0 || oy === void 0)
|
|
387
|
+
return void 0;
|
|
388
|
+
return Math.hypot(px - ox, py - oy);
|
|
389
|
+
}
|
|
390
|
+
function nearestConnected(core, collection, id) {
|
|
391
|
+
const c = core.ext.collections.find((x) => x.name === collection);
|
|
392
|
+
const policy = c?.ownership;
|
|
393
|
+
if (!policy?.position) return void 0;
|
|
394
|
+
const object = plainEntity(core.plain, collection).get(id);
|
|
395
|
+
let best;
|
|
396
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
397
|
+
for (const clientId of connectedClients(core)) {
|
|
398
|
+
const d = distanceTo(core, policy, object, clientId);
|
|
399
|
+
if (d === void 0 || d >= bestDistance) continue;
|
|
400
|
+
bestDistance = d;
|
|
401
|
+
best = clientId;
|
|
402
|
+
}
|
|
403
|
+
return best;
|
|
404
|
+
}
|
|
405
|
+
function addPerPlayerRecords(core, clientId) {
|
|
406
|
+
for (const c of core.ext.collections) {
|
|
407
|
+
if (c.kind !== "entity" || !c.perPlayer) continue;
|
|
408
|
+
const coll = plainEntity(core.plain, c.name);
|
|
409
|
+
if (coll.has(clientId)) {
|
|
410
|
+
if (coll.ownerOf(clientId) !== clientId) {
|
|
411
|
+
trackedEntity(core, c.name).setOwner(clientId, clientId);
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
trackedEntity(core, c.name).add(clientId, defaultRecord(c), { owner: clientId });
|
|
417
|
+
} catch (err) {
|
|
418
|
+
core.log("error", `perPlayer add on ${c.name} failed for ${clientId}:`, err);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function resolveTransfer(core, clientId, c, id) {
|
|
423
|
+
const policy = c.ownership;
|
|
424
|
+
const coll = plainEntity(core.plain, c.name);
|
|
425
|
+
const owner = coll.ownerOf(id);
|
|
426
|
+
if (owner === clientId) return "granted";
|
|
427
|
+
const hold = holdOn(core, c.name, id);
|
|
428
|
+
if (hold) {
|
|
429
|
+
const playerId = core.clients.get(clientId)?.playerId ?? clientId;
|
|
430
|
+
if (hold.holder !== playerId) return "refused";
|
|
431
|
+
reclaimHolds(core, clientId, playerId);
|
|
432
|
+
return coll.ownerOf(id) === clientId ? "granted" : "refused";
|
|
433
|
+
}
|
|
434
|
+
switch (policy.transfer) {
|
|
435
|
+
case "never":
|
|
436
|
+
return "refused";
|
|
437
|
+
case "free":
|
|
438
|
+
trackedEntity(core, c.name).setOwner(id, clientId);
|
|
439
|
+
return "granted";
|
|
440
|
+
case "closer": {
|
|
441
|
+
if (owner === void 0 || owner === SERVER_OWNER) {
|
|
442
|
+
trackedEntity(core, c.name).setOwner(id, clientId);
|
|
443
|
+
return "granted";
|
|
444
|
+
}
|
|
445
|
+
const object = coll.get(id);
|
|
446
|
+
const ownerDistance = distanceTo(core, policy, object, owner);
|
|
447
|
+
if (ownerDistance === void 0) {
|
|
448
|
+
trackedEntity(core, c.name).setOwner(id, clientId);
|
|
449
|
+
return "granted";
|
|
450
|
+
}
|
|
451
|
+
const mine = distanceTo(core, policy, object, clientId);
|
|
452
|
+
if (mine === void 0) return "refused";
|
|
453
|
+
if (mine + policy.by >= ownerDistance) return "refused";
|
|
454
|
+
trackedEntity(core, c.name).setOwner(id, clientId);
|
|
455
|
+
return "granted";
|
|
456
|
+
}
|
|
457
|
+
case "ask": {
|
|
458
|
+
const s = stateOf2(core);
|
|
459
|
+
if (!s.askWarned) {
|
|
460
|
+
s.askWarned = true;
|
|
461
|
+
core.log(
|
|
462
|
+
"warn",
|
|
463
|
+
`${c.name}: ownership transfer 'ask' is not yet forwarded to owners; requests are refused (the same answer an owner who does not reply gives). Use 'closer' or 'free' for now.`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return "refused";
|
|
467
|
+
}
|
|
468
|
+
default:
|
|
469
|
+
return "refused";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function serializeHolds(core) {
|
|
473
|
+
const s = states2.get(core);
|
|
474
|
+
if (!s || s.holds.size === 0) return void 0;
|
|
475
|
+
const json = JSON.stringify([...s.holds.values()]);
|
|
476
|
+
return new TextEncoder().encode(json);
|
|
477
|
+
}
|
|
478
|
+
function restoreHolds(core, bytes) {
|
|
479
|
+
const s = stateOf2(core);
|
|
480
|
+
s.holds.clear();
|
|
481
|
+
if (!bytes || bytes.length === 0) return;
|
|
482
|
+
try {
|
|
483
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
484
|
+
for (const h of parsed) {
|
|
485
|
+
if (typeof h?.collection !== "string" || typeof h.id !== "string" || typeof h.holder !== "string" || typeof h.deadlineMs !== "number") {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
s.holds.set(keyOf(h.collection, h.id), h);
|
|
489
|
+
}
|
|
490
|
+
} catch (err) {
|
|
491
|
+
core.log("warn", "hold deadlines did not decode; the room wakes holding nothing:", err);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
84
495
|
// src/core/rpc.ts
|
|
85
496
|
var RPC_TIMEOUT_MS = 5e3;
|
|
86
497
|
var REQUEST_OWNERSHIP = "requestOwnership";
|
|
87
|
-
var
|
|
88
|
-
function
|
|
89
|
-
let s =
|
|
498
|
+
var states3 = /* @__PURE__ */ new WeakMap();
|
|
499
|
+
function stateOf3(core) {
|
|
500
|
+
let s = states3.get(core);
|
|
90
501
|
if (!s) {
|
|
91
502
|
s = { nextReqId: 1, pending: /* @__PURE__ */ new Map() };
|
|
92
|
-
|
|
503
|
+
states3.set(core, s);
|
|
93
504
|
}
|
|
94
505
|
return s;
|
|
95
506
|
}
|
|
@@ -101,7 +512,7 @@ function errorText(code, detail) {
|
|
|
101
512
|
}
|
|
102
513
|
function sendReply(core, clientId, reqId, result, error) {
|
|
103
514
|
const payload = result === null ? encodeReply({ reqId, ok: false, error: error ?? errorText("E_INTERNAL") }) : encodeReply({ reqId, ok: true, result });
|
|
104
|
-
core.send(clientId,
|
|
515
|
+
core.send(clientId, encodeFrame2(FrameType2.REPLY, payload));
|
|
105
516
|
}
|
|
106
517
|
function handleCall(core, clientId, payload) {
|
|
107
518
|
let call;
|
|
@@ -184,6 +595,9 @@ function runRequestOwnership(core, clientId, ctx, params) {
|
|
|
184
595
|
if (!plain.has(id)) return { granted: false };
|
|
185
596
|
const handler = core.definition.config.onOwnershipRequest;
|
|
186
597
|
const tracked = trackedEntity(core, name);
|
|
598
|
+
if (!handler && c.ownership) {
|
|
599
|
+
return { granted: resolveTransfer(core, clientId, c, id) === "granted" };
|
|
600
|
+
}
|
|
187
601
|
if (handler) {
|
|
188
602
|
const ran = core.tryRun("onOwnershipRequest", () => handler(core.anyState, name, id, ctx));
|
|
189
603
|
if (ran.ok && ran.value === true && plain.ownerOf(id) !== clientId) {
|
|
@@ -193,7 +607,7 @@ function runRequestOwnership(core, clientId, ctx, params) {
|
|
|
193
607
|
}
|
|
194
608
|
const owner = plain.ownerOf(id);
|
|
195
609
|
if (owner === clientId) return { granted: true };
|
|
196
|
-
if (owner !==
|
|
610
|
+
if (owner !== SERVER_OWNER2) return { granted: false };
|
|
197
611
|
tracked.setOwner(id, clientId);
|
|
198
612
|
return { granted: true };
|
|
199
613
|
}
|
|
@@ -206,13 +620,13 @@ function descFor(core, name) {
|
|
|
206
620
|
return found;
|
|
207
621
|
}
|
|
208
622
|
function sendCall(core, clientId, desc, params) {
|
|
209
|
-
const s =
|
|
623
|
+
const s = stateOf3(core);
|
|
210
624
|
const reqId = s.nextReqId;
|
|
211
625
|
s.nextReqId = s.nextReqId + 1 >>> 0 || 1;
|
|
212
626
|
const bytes = encodeFields(desc.params, params);
|
|
213
627
|
core.send(
|
|
214
628
|
clientId,
|
|
215
|
-
|
|
629
|
+
encodeFrame2(FrameType2.CALL, encodeCall({ reqId, rpcId: desc.index, params: bytes }))
|
|
216
630
|
);
|
|
217
631
|
return reqId;
|
|
218
632
|
}
|
|
@@ -232,7 +646,7 @@ function createCallProxy(core, clientId) {
|
|
|
232
646
|
return;
|
|
233
647
|
}
|
|
234
648
|
const reqId = sendCall(core, clientId, desc, params ?? {});
|
|
235
|
-
const s =
|
|
649
|
+
const s = stateOf3(core);
|
|
236
650
|
s.pending.set(pendingKey(clientId, reqId), {
|
|
237
651
|
clientId,
|
|
238
652
|
name: desc.name,
|
|
@@ -280,7 +694,7 @@ function handleReply(core, clientId, payload) {
|
|
|
280
694
|
core.log("warn", `REPLY from ${clientId} failed to decode:`, err);
|
|
281
695
|
return false;
|
|
282
696
|
}
|
|
283
|
-
const s =
|
|
697
|
+
const s = stateOf3(core);
|
|
284
698
|
const key = pendingKey(clientId, reply.reqId);
|
|
285
699
|
const pending = s.pending.get(key);
|
|
286
700
|
if (!pending) {
|
|
@@ -304,7 +718,7 @@ function handleReply(core, clientId, payload) {
|
|
|
304
718
|
return true;
|
|
305
719
|
}
|
|
306
720
|
function rejectPendingFor(core, clientId, reason) {
|
|
307
|
-
const s =
|
|
721
|
+
const s = stateOf3(core);
|
|
308
722
|
for (const [key, p] of [...s.pending]) {
|
|
309
723
|
if (p.clientId !== clientId) continue;
|
|
310
724
|
s.pending.delete(key);
|
|
@@ -312,7 +726,7 @@ function rejectPendingFor(core, clientId, reason) {
|
|
|
312
726
|
}
|
|
313
727
|
}
|
|
314
728
|
function checkTimeouts(core) {
|
|
315
|
-
const s =
|
|
729
|
+
const s = stateOf3(core);
|
|
316
730
|
if (s.pending.size === 0) return;
|
|
317
731
|
const now = core.host.now();
|
|
318
732
|
for (const [key, p] of [...s.pending]) {
|
|
@@ -322,7 +736,7 @@ function checkTimeouts(core) {
|
|
|
322
736
|
}
|
|
323
737
|
}
|
|
324
738
|
function rejectAllPending(core, reason) {
|
|
325
|
-
const s =
|
|
739
|
+
const s = stateOf3(core);
|
|
326
740
|
for (const [key, p] of [...s.pending]) {
|
|
327
741
|
s.pending.delete(key);
|
|
328
742
|
p.reject(new Error(reason));
|
|
@@ -330,15 +744,15 @@ function rejectAllPending(core, reason) {
|
|
|
330
744
|
}
|
|
331
745
|
|
|
332
746
|
// src/core/loop.ts
|
|
333
|
-
import { FrameType as
|
|
747
|
+
import { FrameType as FrameType3 } from "@irtio/protocol";
|
|
334
748
|
|
|
335
749
|
// src/core/host-calls.ts
|
|
336
|
-
var
|
|
337
|
-
function
|
|
338
|
-
let s =
|
|
750
|
+
var states4 = /* @__PURE__ */ new WeakMap();
|
|
751
|
+
function stateOf4(core) {
|
|
752
|
+
let s = states4.get(core);
|
|
339
753
|
if (!s) {
|
|
340
754
|
s = { nextReqId: 1, pending: /* @__PURE__ */ new Map() };
|
|
341
|
-
|
|
755
|
+
states4.set(core, s);
|
|
342
756
|
}
|
|
343
757
|
return s;
|
|
344
758
|
}
|
|
@@ -351,7 +765,7 @@ function startHostCall(core, call, map) {
|
|
|
351
765
|
return rejectAsEvent(core, new Error(`room.${call.kind}: the room is stopped`));
|
|
352
766
|
}
|
|
353
767
|
return new Promise((resolve, reject2) => {
|
|
354
|
-
const s =
|
|
768
|
+
const s = stateOf4(core);
|
|
355
769
|
const reqId = s.nextReqId;
|
|
356
770
|
s.nextReqId = s.nextReqId + 1 >>> 0 || 1;
|
|
357
771
|
s.pending.set(reqId, {
|
|
@@ -375,7 +789,7 @@ function startHostCall(core, call, map) {
|
|
|
375
789
|
});
|
|
376
790
|
}
|
|
377
791
|
function completeHostCall(core, reqId, result) {
|
|
378
|
-
const s =
|
|
792
|
+
const s = stateOf4(core);
|
|
379
793
|
const pending = s.pending.get(reqId);
|
|
380
794
|
if (!pending) return false;
|
|
381
795
|
s.pending.delete(reqId);
|
|
@@ -384,7 +798,7 @@ function completeHostCall(core, reqId, result) {
|
|
|
384
798
|
return true;
|
|
385
799
|
}
|
|
386
800
|
function checkHostCallTimeouts(core) {
|
|
387
|
-
const s =
|
|
801
|
+
const s = stateOf4(core);
|
|
388
802
|
if (s.pending.size === 0) return;
|
|
389
803
|
const now = core.host.now();
|
|
390
804
|
for (const [reqId, p] of [...s.pending]) {
|
|
@@ -394,7 +808,7 @@ function checkHostCallTimeouts(core) {
|
|
|
394
808
|
}
|
|
395
809
|
}
|
|
396
810
|
function rejectAllHostCalls(core, reason) {
|
|
397
|
-
const s =
|
|
811
|
+
const s = stateOf4(core);
|
|
398
812
|
for (const [reqId, p] of [...s.pending]) {
|
|
399
813
|
s.pending.delete(reqId);
|
|
400
814
|
p.reject(new Error(reason));
|
|
@@ -403,7 +817,7 @@ function rejectAllHostCalls(core, reason) {
|
|
|
403
817
|
|
|
404
818
|
// src/core/writes.ts
|
|
405
819
|
import {
|
|
406
|
-
SERVER_OWNER as
|
|
820
|
+
SERVER_OWNER as SERVER_OWNER3,
|
|
407
821
|
cloneValue,
|
|
408
822
|
decodeDelta,
|
|
409
823
|
encodeDelta,
|
|
@@ -493,6 +907,9 @@ function applyWrite(core, clientId, payload) {
|
|
|
493
907
|
core.log("warn", `WRITE from ${clientId} failed to decode:`, err);
|
|
494
908
|
return { ok: false };
|
|
495
909
|
}
|
|
910
|
+
let ops = 0;
|
|
911
|
+
for (const dc of delta.collections) ops += dc.ops.length;
|
|
912
|
+
if (!admitWrite(core, clientId, core.host.now(), ops)) return { ok: true };
|
|
496
913
|
const judged = core.clients.get(clientId);
|
|
497
914
|
if (judged && delta.tick > judged.lastClientTick) {
|
|
498
915
|
judged.lastClientTick = delta.tick;
|
|
@@ -513,8 +930,19 @@ function reject(core, clientId, c, op, reason) {
|
|
|
513
930
|
correctLeaves(core, clientId, c.name, op.id, leafPaths(fieldList(c), op.mask));
|
|
514
931
|
}
|
|
515
932
|
function applyOp(core, clientId, c, op) {
|
|
516
|
-
if (op.op
|
|
517
|
-
|
|
933
|
+
if (op.op === "add" || op.op === "remove") {
|
|
934
|
+
if (c.kind !== "entity" || c.serverOwned || !c.clientCreate) {
|
|
935
|
+
reject(
|
|
936
|
+
core,
|
|
937
|
+
clientId,
|
|
938
|
+
c,
|
|
939
|
+
op,
|
|
940
|
+
`op '${op.op}' is not allowed in a WRITE (${c.name} does not declare clientCreate)`
|
|
941
|
+
);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (op.op === "add") applyAdd(core, clientId, c, op);
|
|
945
|
+
else applyRemove(core, clientId, c, op);
|
|
518
946
|
return;
|
|
519
947
|
}
|
|
520
948
|
if (c.kind !== "entity") {
|
|
@@ -537,6 +965,8 @@ function applyOp(core, clientId, c, op) {
|
|
|
537
965
|
return;
|
|
538
966
|
}
|
|
539
967
|
const fields = fieldList(c);
|
|
968
|
+
const growth = updateGrowth(c, leafPaths(fields, op.mask));
|
|
969
|
+
if (growth > 0 && !admitGrowth(core, clientId, growth)) return;
|
|
540
970
|
const prev = cloneValue(current);
|
|
541
971
|
const next = cloneValue(current);
|
|
542
972
|
applyMaskedPatch(fields, next, op.patch, op.mask);
|
|
@@ -594,6 +1024,66 @@ function applyOp(core, clientId, c, op) {
|
|
|
594
1024
|
);
|
|
595
1025
|
correctLeaves(core, clientId, c.name, op.id, corrected);
|
|
596
1026
|
}
|
|
1027
|
+
function applyAdd(core, clientId, c, op) {
|
|
1028
|
+
const plainColl = plainEntity(core.plain, c.name);
|
|
1029
|
+
if (plainColl.has(op.id)) {
|
|
1030
|
+
reject(core, clientId, c, op, `id ${JSON.stringify(op.id)} already exists`);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const cap = recordCap(core);
|
|
1034
|
+
if (plainColl.size >= cap) {
|
|
1035
|
+
refuseByName(core, clientId, "E_RECORD_CAP", { entity: c.name, limit: cap });
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (!admitGrowth(core, clientId, maxRecordBytes(c))) return;
|
|
1039
|
+
try {
|
|
1040
|
+
trackedEntity(core, c.name).add(op.id, op.value, { owner: clientId });
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
reject(core, clientId, c, op, `add rejected: ${String(err)}`);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
core.recordEvent("write", clientId, `${c.name}: added ${op.id}`);
|
|
1046
|
+
}
|
|
1047
|
+
function applyRemove(core, clientId, c, op) {
|
|
1048
|
+
const plainColl = plainEntity(core.plain, c.name);
|
|
1049
|
+
if (!plainColl.has(op.id)) return;
|
|
1050
|
+
if (plainColl.ownerOf(op.id) !== clientId) {
|
|
1051
|
+
reject(core, clientId, c, op, "not the owner");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
trackedEntity(core, c.name).remove(op.id);
|
|
1055
|
+
core.recordEvent("write", clientId, `${c.name}: removed ${op.id}`);
|
|
1056
|
+
}
|
|
1057
|
+
function updateGrowth(c, leaves) {
|
|
1058
|
+
let growth = 0;
|
|
1059
|
+
for (const leaf of leaves) {
|
|
1060
|
+
const first = leaf.path[0];
|
|
1061
|
+
if (first === void 0) continue;
|
|
1062
|
+
const field = c.fields[first];
|
|
1063
|
+
if (!field) continue;
|
|
1064
|
+
const bound = maxTypeBytes(field.type);
|
|
1065
|
+
if (isFixedWidth(field.type)) continue;
|
|
1066
|
+
growth += bound;
|
|
1067
|
+
}
|
|
1068
|
+
return growth;
|
|
1069
|
+
}
|
|
1070
|
+
function isFixedWidth(t) {
|
|
1071
|
+
switch (t.kind) {
|
|
1072
|
+
case "bool":
|
|
1073
|
+
case "u8":
|
|
1074
|
+
case "u16":
|
|
1075
|
+
case "u32":
|
|
1076
|
+
case "i32":
|
|
1077
|
+
case "f32":
|
|
1078
|
+
case "f64":
|
|
1079
|
+
case "enum":
|
|
1080
|
+
return t.opt !== true;
|
|
1081
|
+
case "struct":
|
|
1082
|
+
return Object.values(t.fields).every(isFixedWidth) && t.opt !== true;
|
|
1083
|
+
default:
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
597
1087
|
function serverWinsCorrections(core, dirty) {
|
|
598
1088
|
const ext = core.ext;
|
|
599
1089
|
for (const [name, cd] of dirty) {
|
|
@@ -604,7 +1094,7 @@ function serverWinsCorrections(core, dirty) {
|
|
|
604
1094
|
const fields = fieldList(c);
|
|
605
1095
|
for (const [id, rd] of cd.updated) {
|
|
606
1096
|
const owner = coll.ownerOf(id);
|
|
607
|
-
if (owner === void 0 || owner ===
|
|
1097
|
+
if (owner === void 0 || owner === SERVER_OWNER3) continue;
|
|
608
1098
|
const entry = core.clients.get(owner);
|
|
609
1099
|
if (!entry || !entry.connected) continue;
|
|
610
1100
|
const value = coll.get(id);
|
|
@@ -700,8 +1190,8 @@ var Loop = class {
|
|
|
700
1190
|
}
|
|
701
1191
|
/** Applies one queued/immediate frame. Returns `false` on a malformed payload. */
|
|
702
1192
|
applyFrame(f) {
|
|
703
|
-
if (f.type ===
|
|
704
|
-
if (f.type ===
|
|
1193
|
+
if (f.type === FrameType3.WRITE) return applyWrite(this.core, f.clientId, f.payload).ok;
|
|
1194
|
+
if (f.type === FrameType3.CALL) return handleCall(this.core, f.clientId, f.payload);
|
|
705
1195
|
return true;
|
|
706
1196
|
}
|
|
707
1197
|
// -------------------------------------------------------------------------
|
|
@@ -2088,7 +2578,7 @@ import {
|
|
|
2088
2578
|
createDirtySet,
|
|
2089
2579
|
createFieldMask,
|
|
2090
2580
|
encodeDelta as encodeDelta2,
|
|
2091
|
-
encodeSnapshot,
|
|
2581
|
+
encodeSnapshot as encodeSnapshot2,
|
|
2092
2582
|
filterDirty,
|
|
2093
2583
|
isDirtyEmpty,
|
|
2094
2584
|
markAdd,
|
|
@@ -2195,7 +2685,7 @@ function viewKeyFor(ext, role) {
|
|
|
2195
2685
|
return infoOf(ext).scoped.has(role) ? role : "all";
|
|
2196
2686
|
}
|
|
2197
2687
|
function encodeViewSnapshot(ext, plain, role, tick, memberships) {
|
|
2198
|
-
return
|
|
2688
|
+
return encodeSnapshot2(
|
|
2199
2689
|
ext,
|
|
2200
2690
|
plain,
|
|
2201
2691
|
{ tick },
|
|
@@ -2325,7 +2815,7 @@ function catchUpDirty(ext, plain, fromRole, toRole) {
|
|
|
2325
2815
|
import { withBuiltins } from "@irtio/protocol";
|
|
2326
2816
|
import { ByteReader as ByteReader3, ByteWriter as ByteWriter3, decodeSnapshot } from "@irtio/schema";
|
|
2327
2817
|
var SNAPSHOT_FORMAT_VERSION = 3;
|
|
2328
|
-
var READABLE_SNAPSHOT_VERSIONS = [1, 2, 3];
|
|
2818
|
+
var READABLE_SNAPSHOT_VERSIONS = [1, 2, 3, 4];
|
|
2329
2819
|
function parseHibernationBlob(bytes) {
|
|
2330
2820
|
const r = new ByteReader3(bytes);
|
|
2331
2821
|
const version = r.u8();
|
|
@@ -2342,7 +2832,12 @@ function parseHibernationBlob(bytes) {
|
|
|
2342
2832
|
const section = r.blob();
|
|
2343
2833
|
if (section.length > 0) physics = section;
|
|
2344
2834
|
}
|
|
2345
|
-
|
|
2835
|
+
let holds;
|
|
2836
|
+
if (version >= 4) {
|
|
2837
|
+
const section = r.blob();
|
|
2838
|
+
if (section.length > 0) holds = section;
|
|
2839
|
+
}
|
|
2840
|
+
return { version, seed, rngState, tick, mode, schemaHash8, physics, holds, snapshot: r.rest() };
|
|
2346
2841
|
}
|
|
2347
2842
|
function sameSchemaHash(a, b) {
|
|
2348
2843
|
if (!a || !b) return true;
|
|
@@ -2350,10 +2845,10 @@ function sameSchemaHash(a, b) {
|
|
|
2350
2845
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2351
2846
|
return true;
|
|
2352
2847
|
}
|
|
2353
|
-
function writeHibernationBlob(header, snapshot, physics) {
|
|
2848
|
+
function writeHibernationBlob(header, snapshot, physics, holds) {
|
|
2354
2849
|
const hash = header.schemaHash8;
|
|
2355
|
-
const version = hash ? 3 : physics ? 2 : 1;
|
|
2356
|
-
const w = new ByteWriter3(snapshot.length + (physics?.length ?? 0) +
|
|
2850
|
+
const version = holds && holds.length > 0 ? 4 : hash ? 3 : physics ? 2 : 1;
|
|
2851
|
+
const w = new ByteWriter3(snapshot.length + (physics?.length ?? 0) + (holds?.length ?? 0) + 40);
|
|
2357
2852
|
w.u8(version);
|
|
2358
2853
|
w.u32(header.seed >>> 0);
|
|
2359
2854
|
w.u32(header.rngState >>> 0);
|
|
@@ -2361,6 +2856,7 @@ function writeHibernationBlob(header, snapshot, physics) {
|
|
|
2361
2856
|
w.u8(header.mode === "event" ? 1 : 0);
|
|
2362
2857
|
if (hash) w.bytes(hash);
|
|
2363
2858
|
if (version >= 2) w.blob(physics ?? new Uint8Array(0));
|
|
2859
|
+
if (version >= 4) w.blob(holds ?? new Uint8Array(0));
|
|
2364
2860
|
w.bytes(snapshot);
|
|
2365
2861
|
return w.finish();
|
|
2366
2862
|
}
|
|
@@ -2434,13 +2930,13 @@ var TimelineRecorder = class {
|
|
|
2434
2930
|
|
|
2435
2931
|
// src/core/room.ts
|
|
2436
2932
|
import {
|
|
2437
|
-
FrameType as
|
|
2933
|
+
FrameType as FrameType6,
|
|
2438
2934
|
PRESENCE_COLLECTION,
|
|
2439
2935
|
ProfileLedger,
|
|
2440
2936
|
busChannelProblem as busChannelProblem2,
|
|
2441
2937
|
decodeFrame,
|
|
2442
2938
|
encodeCorrectFrame,
|
|
2443
|
-
encodeFrame as
|
|
2939
|
+
encodeFrame as encodeFrame5,
|
|
2444
2940
|
withBuiltins as withBuiltins2
|
|
2445
2941
|
} from "@irtio/protocol";
|
|
2446
2942
|
import {
|
|
@@ -2448,9 +2944,9 @@ import {
|
|
|
2448
2944
|
createDirtySet as createDirtySet2,
|
|
2449
2945
|
createState,
|
|
2450
2946
|
decodeSnapshot as decodeSnapshot2,
|
|
2451
|
-
encodeDelta as
|
|
2452
|
-
encodeSnapshot as
|
|
2453
|
-
isDirtyEmpty as
|
|
2947
|
+
encodeDelta as encodeDelta5,
|
|
2948
|
+
encodeSnapshot as encodeSnapshot4,
|
|
2949
|
+
isDirtyEmpty as isDirtyEmpty4,
|
|
2454
2950
|
track,
|
|
2455
2951
|
validateForDeploy
|
|
2456
2952
|
} from "@irtio/schema";
|
|
@@ -2709,12 +3205,15 @@ function historyOf(config) {
|
|
|
2709
3205
|
}
|
|
2710
3206
|
return { depth, channels };
|
|
2711
3207
|
}
|
|
2712
|
-
function
|
|
3208
|
+
function keyOf2(collection, id) {
|
|
2713
3209
|
return `${collection} ${id}`;
|
|
2714
3210
|
}
|
|
2715
3211
|
function angleOf(qz, qw) {
|
|
2716
3212
|
return 2 * Math.atan2(qz, qw);
|
|
2717
3213
|
}
|
|
3214
|
+
function mixSignature(h, v) {
|
|
3215
|
+
return Math.imul(h ^ (Math.round(v * 1024) | 0), 2654435761);
|
|
3216
|
+
}
|
|
2718
3217
|
var BaseScratch = class {
|
|
2719
3218
|
constructor(physics, depth, tickNow) {
|
|
2720
3219
|
this.physics = physics;
|
|
@@ -2731,7 +3230,7 @@ var BaseScratch = class {
|
|
|
2731
3230
|
this.sync();
|
|
2732
3231
|
this.present.clear();
|
|
2733
3232
|
for (let i = 0; i < entry.count; i++) {
|
|
2734
|
-
const key =
|
|
3233
|
+
const key = keyOf2(entry.collections[i], entry.ids[i]);
|
|
2735
3234
|
const e = this.bodies.get(key);
|
|
2736
3235
|
if (!e) continue;
|
|
2737
3236
|
poseAt(entry, i, this.pose);
|
|
@@ -2745,7 +3244,8 @@ var BaseScratch = class {
|
|
|
2745
3244
|
}
|
|
2746
3245
|
/**
|
|
2747
3246
|
* Brings the scratch's body set in step with the live world: a double for every live body that
|
|
2748
|
-
* has none, a rebuild for one whose collider
|
|
3247
|
+
* has none, a rebuild for one whose collider layout changed (count, handles, body-local
|
|
3248
|
+
* offsets, shape type or extents, sensor flags — see `signatureOf`), and a prune for one that
|
|
2749
3249
|
* has been gone longer than the history is deep and so can no longer appear in any entry.
|
|
2750
3250
|
*
|
|
2751
3251
|
* The prune is conservative rather than exact: `lastLive` only advances when a rewind happens,
|
|
@@ -2756,11 +3256,11 @@ var BaseScratch = class {
|
|
|
2756
3256
|
const now = this.tickNow();
|
|
2757
3257
|
const seen = /* @__PURE__ */ new Set();
|
|
2758
3258
|
this.physics.eachTrackedBody((collection, id, body) => {
|
|
2759
|
-
const key =
|
|
3259
|
+
const key = keyOf2(collection, id);
|
|
2760
3260
|
seen.add(key);
|
|
2761
|
-
const
|
|
3261
|
+
const signature = this.signatureOf(body);
|
|
2762
3262
|
const existing = this.bodies.get(key);
|
|
2763
|
-
if (existing && existing.
|
|
3263
|
+
if (existing && existing.signature === signature) {
|
|
2764
3264
|
existing.lastLive = now;
|
|
2765
3265
|
return;
|
|
2766
3266
|
}
|
|
@@ -2768,7 +3268,7 @@ var BaseScratch = class {
|
|
|
2768
3268
|
this.bodies.delete(key);
|
|
2769
3269
|
this.destroy(existing);
|
|
2770
3270
|
}
|
|
2771
|
-
const made = this.clone(collection, id, body,
|
|
3271
|
+
const made = this.clone(collection, id, body, signature);
|
|
2772
3272
|
if (!made) return;
|
|
2773
3273
|
made.lastLive = now;
|
|
2774
3274
|
this.bodies.set(key, made);
|
|
@@ -2809,16 +3309,46 @@ var RapierScratch = class extends BaseScratch {
|
|
|
2809
3309
|
this.world = new this.rapier.World({ x: 0, y: 0, z: 0 });
|
|
2810
3310
|
this.world.timestep = 0;
|
|
2811
3311
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
3312
|
+
signatureOf(body) {
|
|
3313
|
+
const live = body;
|
|
3314
|
+
const n = live.numColliders();
|
|
3315
|
+
const origin = live.translation();
|
|
3316
|
+
const inv = qConj(live.rotation());
|
|
3317
|
+
let h = mixSignature(0, n);
|
|
3318
|
+
for (let i = 0; i < n; i++) {
|
|
3319
|
+
const c = live.collider(i);
|
|
3320
|
+
h = mixSignature(h, c.handle);
|
|
3321
|
+
h = mixSignature(h, c.isSensor() ? 1 : 0);
|
|
3322
|
+
const w = c.translation();
|
|
3323
|
+
const local = qRotate(inv, { x: w.x - origin.x, y: w.y - origin.y, z: w.z - origin.z });
|
|
3324
|
+
h = mixSignature(h, local.x);
|
|
3325
|
+
h = mixSignature(h, local.y);
|
|
3326
|
+
h = mixSignature(h, local.z);
|
|
3327
|
+
const r = qMul(inv, c.rotation());
|
|
3328
|
+
h = mixSignature(h, r.x);
|
|
3329
|
+
h = mixSignature(h, r.y);
|
|
3330
|
+
h = mixSignature(h, r.z);
|
|
3331
|
+
h = mixSignature(h, r.w);
|
|
3332
|
+
const s = c.shape;
|
|
3333
|
+
h = mixSignature(h, s.type);
|
|
3334
|
+
h = mixSignature(h, s.radius ?? 0);
|
|
3335
|
+
h = mixSignature(h, s.halfHeight ?? 0);
|
|
3336
|
+
if (s.halfExtents) {
|
|
3337
|
+
h = mixSignature(h, s.halfExtents.x);
|
|
3338
|
+
h = mixSignature(h, s.halfExtents.y);
|
|
3339
|
+
h = mixSignature(h, s.halfExtents.z);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return h;
|
|
2814
3343
|
}
|
|
2815
|
-
clone(collection, id, body,
|
|
3344
|
+
clone(collection, id, body, signature) {
|
|
2816
3345
|
const live = body;
|
|
2817
3346
|
this.trackedLive.add(live.handle);
|
|
2818
3347
|
const made = this.world.createRigidBody(this.rapier.RigidBodyDesc.fixed());
|
|
2819
3348
|
const origin = live.translation();
|
|
2820
3349
|
const rot = live.rotation();
|
|
2821
3350
|
const inv = qConj(rot);
|
|
3351
|
+
const parts = live.numColliders();
|
|
2822
3352
|
for (let i = 0; i < parts; i++) {
|
|
2823
3353
|
const c = live.collider(i);
|
|
2824
3354
|
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
@@ -2830,7 +3360,7 @@ var RapierScratch = class extends BaseScratch {
|
|
|
2830
3360
|
this.world.createCollider(desc, made);
|
|
2831
3361
|
}
|
|
2832
3362
|
this.owners.set(made.handle, { collection, id });
|
|
2833
|
-
return { collection, id, body: made,
|
|
3363
|
+
return { collection, id, body: made, signature, lastLive: -1 };
|
|
2834
3364
|
}
|
|
2835
3365
|
place(entry, pose) {
|
|
2836
3366
|
const b = entry.body;
|
|
@@ -2906,10 +3436,36 @@ var Rapier2dScratch = class extends BaseScratch {
|
|
|
2906
3436
|
this.world = new this.rapier.World({ x: 0, y: 0 });
|
|
2907
3437
|
this.world.timestep = 0;
|
|
2908
3438
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
3439
|
+
signatureOf(body) {
|
|
3440
|
+
const live = body;
|
|
3441
|
+
const n = live.numColliders();
|
|
3442
|
+
const origin = live.translation();
|
|
3443
|
+
const angle = live.rotation();
|
|
3444
|
+
const cos = Math.cos(-angle);
|
|
3445
|
+
const sin = Math.sin(-angle);
|
|
3446
|
+
let h = mixSignature(0, n);
|
|
3447
|
+
for (let i = 0; i < n; i++) {
|
|
3448
|
+
const c = live.collider(i);
|
|
3449
|
+
h = mixSignature(h, c.handle);
|
|
3450
|
+
h = mixSignature(h, c.isSensor() ? 1 : 0);
|
|
3451
|
+
const w = c.translation();
|
|
3452
|
+
const dx = w.x - origin.x;
|
|
3453
|
+
const dy = w.y - origin.y;
|
|
3454
|
+
h = mixSignature(h, dx * cos - dy * sin);
|
|
3455
|
+
h = mixSignature(h, dx * sin + dy * cos);
|
|
3456
|
+
h = mixSignature(h, c.rotation() - angle);
|
|
3457
|
+
const s = c.shape;
|
|
3458
|
+
h = mixSignature(h, s.type);
|
|
3459
|
+
h = mixSignature(h, s.radius ?? 0);
|
|
3460
|
+
h = mixSignature(h, s.halfHeight ?? 0);
|
|
3461
|
+
if (s.halfExtents) {
|
|
3462
|
+
h = mixSignature(h, s.halfExtents.x);
|
|
3463
|
+
h = mixSignature(h, s.halfExtents.y);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
return h;
|
|
2911
3467
|
}
|
|
2912
|
-
clone(collection, id, body,
|
|
3468
|
+
clone(collection, id, body, signature) {
|
|
2913
3469
|
const live = body;
|
|
2914
3470
|
this.trackedLive.add(live.handle);
|
|
2915
3471
|
const made = this.world.createRigidBody(this.rapier.RigidBodyDesc.fixed());
|
|
@@ -2917,6 +3473,7 @@ var Rapier2dScratch = class extends BaseScratch {
|
|
|
2917
3473
|
const angle = live.rotation();
|
|
2918
3474
|
const cos = Math.cos(-angle);
|
|
2919
3475
|
const sin = Math.sin(-angle);
|
|
3476
|
+
const parts = live.numColliders();
|
|
2920
3477
|
for (let i = 0; i < parts; i++) {
|
|
2921
3478
|
const c = live.collider(i);
|
|
2922
3479
|
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
@@ -2929,7 +3486,7 @@ var Rapier2dScratch = class extends BaseScratch {
|
|
|
2929
3486
|
this.world.createCollider(desc, made);
|
|
2930
3487
|
}
|
|
2931
3488
|
this.owners.set(made.handle, { collection, id });
|
|
2932
|
-
return { collection, id, body: made,
|
|
3489
|
+
return { collection, id, body: made, signature, lastLive: -1 };
|
|
2933
3490
|
}
|
|
2934
3491
|
place(entry, pose) {
|
|
2935
3492
|
const b = entry.body;
|
|
@@ -2991,10 +3548,23 @@ var MatterScratch = class extends BaseScratch {
|
|
|
2991
3548
|
super(physics, depth, tickNow);
|
|
2992
3549
|
this.matter = physics.matter;
|
|
2993
3550
|
}
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3551
|
+
signatureOf(body) {
|
|
3552
|
+
const live = body;
|
|
3553
|
+
const cos = Math.cos(-live.angle);
|
|
3554
|
+
const sin = Math.sin(-live.angle);
|
|
3555
|
+
let h = mixSignature(0, live.parts.length);
|
|
3556
|
+
for (const p of live.parts) {
|
|
3557
|
+
h = mixSignature(h, p.area);
|
|
3558
|
+
h = mixSignature(h, p.vertices.length);
|
|
3559
|
+
h = mixSignature(h, p.isSensor ? 1 : 0);
|
|
3560
|
+
const dx = p.position.x - live.position.x;
|
|
3561
|
+
const dy = p.position.y - live.position.y;
|
|
3562
|
+
h = mixSignature(h, dx * cos - dy * sin);
|
|
3563
|
+
h = mixSignature(h, dx * sin + dy * cos);
|
|
3564
|
+
}
|
|
3565
|
+
return h;
|
|
3566
|
+
}
|
|
3567
|
+
clone(collection, id, body, signature) {
|
|
2998
3568
|
const M = this.matter;
|
|
2999
3569
|
const live = body;
|
|
3000
3570
|
const made = live.parts.length > 1 ? M.Body.create({ parts: live.parts.slice(1).map((p) => this.clonePart(p, live.angle)) }) : this.clonePart(live, live.angle);
|
|
@@ -3002,7 +3572,7 @@ var MatterScratch = class extends BaseScratch {
|
|
|
3002
3572
|
made.angle = 0;
|
|
3003
3573
|
for (const p of made.parts) p.angle = 0;
|
|
3004
3574
|
this.owners.set(made, { collection, id });
|
|
3005
|
-
return { collection, id, body: made,
|
|
3575
|
+
return { collection, id, body: made, signature, lastLive: -1 };
|
|
3006
3576
|
}
|
|
3007
3577
|
/** One part, un-rotated by the parent's angle so the double starts at angle 0. */
|
|
3008
3578
|
clonePart(part, parentAngle) {
|
|
@@ -3131,13 +3701,13 @@ function lobbyConfigOf(core) {
|
|
|
3131
3701
|
if (!schemaHasLobby(core.definition.schema)) return void 0;
|
|
3132
3702
|
return declared;
|
|
3133
3703
|
}
|
|
3134
|
-
function
|
|
3704
|
+
function stateOf5(core) {
|
|
3135
3705
|
return core.anyState[LOBBY_STATE];
|
|
3136
3706
|
}
|
|
3137
3707
|
function evaluateLobby(core) {
|
|
3138
3708
|
const config = lobbyConfigOf(core);
|
|
3139
3709
|
if (config === void 0) return;
|
|
3140
|
-
const state =
|
|
3710
|
+
const state = stateOf5(core);
|
|
3141
3711
|
if (state === void 0) return;
|
|
3142
3712
|
const members = trackedEntity(core, LOBBY_MEMBERS);
|
|
3143
3713
|
const policy = config.start ?? "when-full";
|
|
@@ -3176,7 +3746,7 @@ function evaluateLobby(core) {
|
|
|
3176
3746
|
function startLobby(core, why) {
|
|
3177
3747
|
const runtime = runtimeOf(core);
|
|
3178
3748
|
if (runtime.started) return;
|
|
3179
|
-
const state =
|
|
3749
|
+
const state = stateOf5(core);
|
|
3180
3750
|
if (state === void 0 || state.phase === "started") {
|
|
3181
3751
|
runtime.started = true;
|
|
3182
3752
|
return;
|
|
@@ -3199,7 +3769,7 @@ function makeLobby(core) {
|
|
|
3199
3769
|
"room.lobby: this room has no lobby. Spread lobbyCollections into the schema and declare lobby: {} in defineRoom \u2014 both are needed, and defineRoom refuses either one alone"
|
|
3200
3770
|
);
|
|
3201
3771
|
}
|
|
3202
|
-
const state =
|
|
3772
|
+
const state = stateOf5(core);
|
|
3203
3773
|
const members = trackedEntity(core, LOBBY_MEMBERS);
|
|
3204
3774
|
return {
|
|
3205
3775
|
get phase() {
|
|
@@ -3237,7 +3807,7 @@ function makeLobby(core) {
|
|
|
3237
3807
|
function setLobbyPublic(core, value) {
|
|
3238
3808
|
const config = lobbyConfigOf(core);
|
|
3239
3809
|
if (config === void 0) return;
|
|
3240
|
-
const state =
|
|
3810
|
+
const state = stateOf5(core);
|
|
3241
3811
|
if (state === void 0) return;
|
|
3242
3812
|
if (state.phase === "started") {
|
|
3243
3813
|
core.log(
|
|
@@ -3256,7 +3826,7 @@ function setLobbyPublic(core, value) {
|
|
|
3256
3826
|
}
|
|
3257
3827
|
|
|
3258
3828
|
// src/core/messages.ts
|
|
3259
|
-
import { FrameType as
|
|
3829
|
+
import { FrameType as FrameType4, decodeMsg, encodeFrame as encodeFrame3, encodeMsg } from "@irtio/protocol";
|
|
3260
3830
|
import { decodeFields as decodeFields2 } from "@irtio/schema";
|
|
3261
3831
|
function toRoomTarget(target) {
|
|
3262
3832
|
switch (target.kind) {
|
|
@@ -3336,8 +3906,8 @@ function handleMsg(core, clientId, payload) {
|
|
|
3336
3906
|
if (!ran.ok || ran.value === false) return true;
|
|
3337
3907
|
}
|
|
3338
3908
|
if (msg.target.kind === "server") return true;
|
|
3339
|
-
const frame =
|
|
3340
|
-
|
|
3909
|
+
const frame = encodeFrame3(
|
|
3910
|
+
FrameType4.MSG,
|
|
3341
3911
|
encodeMsg({
|
|
3342
3912
|
target: { kind: "client", clientId },
|
|
3343
3913
|
payload: msg.payload,
|
|
@@ -3351,28 +3921,306 @@ function wireTargetOf(target) {
|
|
|
3351
3921
|
return target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
3352
3922
|
}
|
|
3353
3923
|
function sendMessage(core, target, bytes) {
|
|
3354
|
-
const frame =
|
|
3355
|
-
|
|
3924
|
+
const frame = encodeFrame3(
|
|
3925
|
+
FrameType4.MSG,
|
|
3356
3926
|
encodeMsg({ target: { kind: "server" }, payload: bytes })
|
|
3357
3927
|
);
|
|
3358
3928
|
deliver(core, wireTargetOf(target), frame);
|
|
3359
3929
|
}
|
|
3360
3930
|
function sendTypedMessage(core, index, target, payload) {
|
|
3361
|
-
const frame =
|
|
3362
|
-
|
|
3931
|
+
const frame = encodeFrame3(
|
|
3932
|
+
FrameType4.MSG,
|
|
3363
3933
|
encodeMsg({ target: { kind: "server" }, payload, typed: { index } })
|
|
3364
3934
|
);
|
|
3365
3935
|
deliver(core, wireTargetOf(target), frame);
|
|
3366
3936
|
}
|
|
3367
3937
|
|
|
3938
|
+
// src/core/replay.ts
|
|
3939
|
+
import {
|
|
3940
|
+
CLIP_MAX_BYTES,
|
|
3941
|
+
encodeClip
|
|
3942
|
+
} from "@irtio/protocol";
|
|
3943
|
+
import {
|
|
3944
|
+
encodeDelta as encodeDelta3,
|
|
3945
|
+
encodeSnapshot as encodeSnapshot3,
|
|
3946
|
+
isDirtyEmpty as isDirtyEmpty2
|
|
3947
|
+
} from "@irtio/schema";
|
|
3948
|
+
var REPLAY_DEFAULT_SECONDS = 60;
|
|
3949
|
+
var REPLAY_MAX_SECONDS = 300;
|
|
3950
|
+
var REPLAY_RING_MAX_BYTES = 8 * 1024 * 1024;
|
|
3951
|
+
var REPLAY_KEYFRAME_SECONDS = 2;
|
|
3952
|
+
var CLIP_COOLDOWN_MS = 1e3;
|
|
3953
|
+
var ReplayRing = class {
|
|
3954
|
+
constructor(bounds) {
|
|
3955
|
+
this.bounds = bounds;
|
|
3956
|
+
}
|
|
3957
|
+
bounds;
|
|
3958
|
+
items = [];
|
|
3959
|
+
total = 0;
|
|
3960
|
+
/** How many times an oversized segment has emptied the ring. Read through `append`'s return. */
|
|
3961
|
+
wipes = 0;
|
|
3962
|
+
/** The tick the newest keyframe went in at; `-1` while the ring is empty. */
|
|
3963
|
+
lastKeyframeTick = -1;
|
|
3964
|
+
get frames() {
|
|
3965
|
+
return this.items;
|
|
3966
|
+
}
|
|
3967
|
+
get bytes() {
|
|
3968
|
+
return this.total;
|
|
3969
|
+
}
|
|
3970
|
+
get empty() {
|
|
3971
|
+
return this.items.length === 0;
|
|
3972
|
+
}
|
|
3973
|
+
/** How many oversized segments have emptied the ring. A test's handle on the degenerate case. */
|
|
3974
|
+
get wipeCount() {
|
|
3975
|
+
return this.wipes;
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Appends a frame and trims. Returns `true` when the trim had to throw the *whole* ring away
|
|
3979
|
+
* (an oversized segment with nowhere to cut), which the caller has to know about: the ring is
|
|
3980
|
+
* now empty, so anything it was about to append next would head the ring without a keyframe.
|
|
3981
|
+
*/
|
|
3982
|
+
append(frame) {
|
|
3983
|
+
this.items.push(frame);
|
|
3984
|
+
this.total += frame.bytes.length;
|
|
3985
|
+
if (frame.keyframe) this.lastKeyframeTick = frame.tick;
|
|
3986
|
+
const before = this.wipes;
|
|
3987
|
+
this.trim();
|
|
3988
|
+
return this.wipes !== before;
|
|
3989
|
+
}
|
|
3990
|
+
/** Is a keyframe due at `tick`? Always true while the ring is empty. */
|
|
3991
|
+
keyframeDue(tick) {
|
|
3992
|
+
return this.empty || tick - this.lastKeyframeTick >= this.bounds.keyframeEvery;
|
|
3993
|
+
}
|
|
3994
|
+
/** The frames from the newest keyframe at or before `fromTick` to the end. */
|
|
3995
|
+
tail(fromTick) {
|
|
3996
|
+
let start = 0;
|
|
3997
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
3998
|
+
const f = this.items[i];
|
|
3999
|
+
if (f.keyframe && f.tick <= fromTick) start = i;
|
|
4000
|
+
}
|
|
4001
|
+
return this.items.slice(start);
|
|
4002
|
+
}
|
|
4003
|
+
trim() {
|
|
4004
|
+
const last = this.items[this.items.length - 1];
|
|
4005
|
+
if (!last) return;
|
|
4006
|
+
for (; ; ) {
|
|
4007
|
+
const head = this.items[0];
|
|
4008
|
+
if (!head) return;
|
|
4009
|
+
const overBytes = this.total > this.bounds.maxBytes;
|
|
4010
|
+
const overWindow = last.tick - head.tick > this.bounds.windowTicks;
|
|
4011
|
+
if (!overBytes && !overWindow) return;
|
|
4012
|
+
let next = -1;
|
|
4013
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
4014
|
+
if (this.items[i]?.keyframe === true) {
|
|
4015
|
+
next = i;
|
|
4016
|
+
break;
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
if (next === -1) {
|
|
4020
|
+
if (!overBytes) return;
|
|
4021
|
+
this.items = [];
|
|
4022
|
+
this.total = 0;
|
|
4023
|
+
this.lastKeyframeTick = -1;
|
|
4024
|
+
this.wipes++;
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
for (let i = 0; i < next; i++) this.total -= this.items[i].bytes.length;
|
|
4028
|
+
this.items = this.items.slice(next);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
};
|
|
4032
|
+
var states5 = /* @__PURE__ */ new WeakMap();
|
|
4033
|
+
function configOf(core) {
|
|
4034
|
+
const config = core.definition.config;
|
|
4035
|
+
return config.replay;
|
|
4036
|
+
}
|
|
4037
|
+
function replayArmed(core) {
|
|
4038
|
+
return configOf(core) !== void 0;
|
|
4039
|
+
}
|
|
4040
|
+
function stateOf6(core) {
|
|
4041
|
+
const declared = configOf(core);
|
|
4042
|
+
if (declared === void 0) return void 0;
|
|
4043
|
+
let s = states5.get(core);
|
|
4044
|
+
if (!s) {
|
|
4045
|
+
const tickRate = core.definition.config.tickRate ?? 20;
|
|
4046
|
+
const seconds = Math.min(
|
|
4047
|
+
REPLAY_MAX_SECONDS,
|
|
4048
|
+
Math.max(1, Math.floor(declared.seconds ?? REPLAY_DEFAULT_SECONDS))
|
|
4049
|
+
);
|
|
4050
|
+
s = {
|
|
4051
|
+
seconds,
|
|
4052
|
+
view: declared.view ?? "",
|
|
4053
|
+
ring: new ReplayRing({
|
|
4054
|
+
windowTicks: Math.max(1, Math.ceil(seconds * tickRate)),
|
|
4055
|
+
keyframeEvery: Math.max(1, Math.round(REPLAY_KEYFRAME_SECONDS * tickRate)),
|
|
4056
|
+
maxBytes: REPLAY_RING_MAX_BYTES
|
|
4057
|
+
}),
|
|
4058
|
+
lastClipAt: Number.NEGATIVE_INFINITY,
|
|
4059
|
+
clipsTaken: 0,
|
|
4060
|
+
disarmed: false
|
|
4061
|
+
};
|
|
4062
|
+
states5.set(core, s);
|
|
4063
|
+
}
|
|
4064
|
+
return s;
|
|
4065
|
+
}
|
|
4066
|
+
function omniscientMemberships(core) {
|
|
4067
|
+
const out = /* @__PURE__ */ new Map();
|
|
4068
|
+
for (const desc of core.ext.collections) {
|
|
4069
|
+
if (desc.visibility !== "spatial-grid") continue;
|
|
4070
|
+
out.set(desc.name, new Set(plainEntity(core.plain, desc.name).ids()));
|
|
4071
|
+
}
|
|
4072
|
+
return out;
|
|
4073
|
+
}
|
|
4074
|
+
function hasRoleScoped(core) {
|
|
4075
|
+
return core.ext.collections.some((c) => c.visibility === "role");
|
|
4076
|
+
}
|
|
4077
|
+
function hasSpatial(core) {
|
|
4078
|
+
return core.ext.collections.some(
|
|
4079
|
+
(c) => c.visibility === "spatial-grid"
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4082
|
+
function recorderSnapshot(core, state) {
|
|
4083
|
+
if (state.view === "") {
|
|
4084
|
+
return encodeSnapshot3(core.ext, core.plain, { tick: core.tick });
|
|
4085
|
+
}
|
|
4086
|
+
return encodeViewSnapshot(
|
|
4087
|
+
core.ext,
|
|
4088
|
+
core.plain,
|
|
4089
|
+
state.view,
|
|
4090
|
+
core.tick,
|
|
4091
|
+
omniscientMemberships(core)
|
|
4092
|
+
);
|
|
4093
|
+
}
|
|
4094
|
+
function recorderDelta(core, state, dirty) {
|
|
4095
|
+
if (state.view === "") {
|
|
4096
|
+
if (isDirtyEmpty2(dirty)) return null;
|
|
4097
|
+
return encodeDelta3(core.ext, core.plain, dirty, { tick: core.tick });
|
|
4098
|
+
}
|
|
4099
|
+
return encodeViewDelta(core.ext, core.plain, dirty, state.view, core.tick);
|
|
4100
|
+
}
|
|
4101
|
+
function cacheKeyFor(core, state) {
|
|
4102
|
+
if (hasSpatial(core)) return void 0;
|
|
4103
|
+
if (state.view === "") return hasRoleScoped(core) ? void 0 : "all";
|
|
4104
|
+
return viewKeyFor(core.ext, state.view);
|
|
4105
|
+
}
|
|
4106
|
+
function recordFlush(core, dirty, byView) {
|
|
4107
|
+
const state = stateOf6(core);
|
|
4108
|
+
if (!state || state.disarmed) return;
|
|
4109
|
+
const ring = state.ring;
|
|
4110
|
+
if (!ring.empty && isDirtyEmpty2(dirty)) return;
|
|
4111
|
+
if (ring.keyframeDue(core.tick)) {
|
|
4112
|
+
const wiped = ring.append({
|
|
4113
|
+
tick: core.tick,
|
|
4114
|
+
keyframe: true,
|
|
4115
|
+
bytes: recorderSnapshot(core, state)
|
|
4116
|
+
});
|
|
4117
|
+
if (wiped) {
|
|
4118
|
+
disarm(core, state);
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
const key = cacheKeyFor(core, state);
|
|
4123
|
+
const cached = key === void 0 ? void 0 : byView.get(key);
|
|
4124
|
+
const delta = cached !== void 0 ? cached : recorderDelta(core, state, dirty);
|
|
4125
|
+
if (!delta) return;
|
|
4126
|
+
ring.append({ tick: core.tick, keyframe: false, bytes: delta });
|
|
4127
|
+
}
|
|
4128
|
+
function disarm(core, state) {
|
|
4129
|
+
state.disarmed = true;
|
|
4130
|
+
core.host.log("warn", [
|
|
4131
|
+
`irtio: room ${core.roomId}: a single replay keyframe is larger than the replay ring's ${REPLAY_RING_MAX_BYTES}-byte bound, so nothing could ever be held. Recording is off for the life of this room and room.replay.clip() will refuse. Record a narrower view (\`replay: { view: <role> }\`) or hold less state in the recorded collections.`
|
|
4132
|
+
]);
|
|
4133
|
+
}
|
|
4134
|
+
function assembleClip(core, seconds) {
|
|
4135
|
+
const state = stateOf6(core);
|
|
4136
|
+
if (!state || state.disarmed || state.ring.empty) return void 0;
|
|
4137
|
+
const tickRate = core.definition.config.tickRate ?? 20;
|
|
4138
|
+
const want = Math.min(state.seconds, Math.max(1, Math.floor(seconds ?? state.seconds)));
|
|
4139
|
+
const ringFrames = state.ring.frames;
|
|
4140
|
+
const last = ringFrames[ringFrames.length - 1];
|
|
4141
|
+
const frames = state.ring.tail(last.tick - Math.ceil(want * tickRate)).map((f) => ({
|
|
4142
|
+
tick: f.tick,
|
|
4143
|
+
kind: f.keyframe ? "keyframe" : "delta",
|
|
4144
|
+
bytes: f.bytes
|
|
4145
|
+
}));
|
|
4146
|
+
const head = frames[0];
|
|
4147
|
+
if (head === void 0 || head.kind !== "keyframe") return void 0;
|
|
4148
|
+
const clip = {
|
|
4149
|
+
// The GAME schema's hash, not the extended one's. That is what a client puts in its HELLO,
|
|
4150
|
+
// so it is the value playback can actually compare against — and it is the hash a deploy
|
|
4151
|
+
// changes, which is the whole thing the pin exists to catch.
|
|
4152
|
+
schemaHash8: core.definition.schema.hash8,
|
|
4153
|
+
tickRate,
|
|
4154
|
+
startTick: head.tick,
|
|
4155
|
+
durationTicks: last.tick - head.tick,
|
|
4156
|
+
view: state.view,
|
|
4157
|
+
frames
|
|
4158
|
+
};
|
|
4159
|
+
return encodeClip(clip);
|
|
4160
|
+
}
|
|
4161
|
+
function takeClip(core, seconds) {
|
|
4162
|
+
if (!replayArmed(core)) {
|
|
4163
|
+
return rejectAsEvent(
|
|
4164
|
+
core,
|
|
4165
|
+
new Error(
|
|
4166
|
+
"room.replay.clip: this room type did not declare `replay:`, so nothing is being recorded. Add `replay: { seconds: 60 }` to the room config to arm the ring."
|
|
4167
|
+
)
|
|
4168
|
+
);
|
|
4169
|
+
}
|
|
4170
|
+
const state = stateOf6(core);
|
|
4171
|
+
const now = core.host.now();
|
|
4172
|
+
if (now - state.lastClipAt < CLIP_COOLDOWN_MS) {
|
|
4173
|
+
return rejectAsEvent(
|
|
4174
|
+
core,
|
|
4175
|
+
new Error(
|
|
4176
|
+
`E_CLIP_COOLDOWN: room.replay.clip may be called once every ${CLIP_COOLDOWN_MS}ms per room; this call came too soon after the last one`
|
|
4177
|
+
)
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
if (state.disarmed) {
|
|
4181
|
+
return rejectAsEvent(
|
|
4182
|
+
core,
|
|
4183
|
+
new Error(
|
|
4184
|
+
`E_CLIP_DISARMED: recording stopped in this room because a single replay keyframe was larger than the ${REPLAY_RING_MAX_BYTES}-byte replay ring; see the warning on this room's log`
|
|
4185
|
+
)
|
|
4186
|
+
);
|
|
4187
|
+
}
|
|
4188
|
+
let bytes;
|
|
4189
|
+
try {
|
|
4190
|
+
bytes = assembleClip(core, seconds);
|
|
4191
|
+
} catch (err) {
|
|
4192
|
+
return rejectAsEvent(
|
|
4193
|
+
core,
|
|
4194
|
+
new Error(`E_CLIP_ENCODE: the replay ring could not be encoded: ${String(err)}`)
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
if (bytes === void 0) {
|
|
4198
|
+
return rejectAsEvent(
|
|
4199
|
+
core,
|
|
4200
|
+
new Error("E_CLIP_EMPTY: the replay ring is empty \u2014 this room has not flushed a change yet")
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
if (bytes.length > CLIP_MAX_BYTES) {
|
|
4204
|
+
return rejectAsEvent(
|
|
4205
|
+
core,
|
|
4206
|
+
new Error(
|
|
4207
|
+
`E_CLIP_TOO_LARGE: this clip is ${bytes.length} bytes and the limit is ${CLIP_MAX_BYTES}`
|
|
4208
|
+
)
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4211
|
+
state.lastClipAt = now;
|
|
4212
|
+
state.clipsTaken++;
|
|
4213
|
+
return startHostCall(core, { kind: "clip", bytes }, (value) => ({ id: value ?? "" }));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
3368
4216
|
// src/core/room-api.ts
|
|
3369
4217
|
import {
|
|
3370
|
-
FrameType as
|
|
4218
|
+
FrameType as FrameType5,
|
|
3371
4219
|
busChannelProblem,
|
|
3372
4220
|
busPayloadProblem,
|
|
3373
|
-
encodeFrame as
|
|
4221
|
+
encodeFrame as encodeFrame4
|
|
3374
4222
|
} from "@irtio/protocol";
|
|
3375
|
-
import { encodeDelta as
|
|
4223
|
+
import { encodeDelta as encodeDelta4, encodeFields as encodeFields2, isDirtyEmpty as isDirtyEmpty3 } from "@irtio/schema";
|
|
3376
4224
|
function makePhysicsApi(p) {
|
|
3377
4225
|
return {
|
|
3378
4226
|
get rapier() {
|
|
@@ -3722,6 +4570,20 @@ function createRoomApi(core, publicUrl) {
|
|
|
3722
4570
|
get lobby() {
|
|
3723
4571
|
return makeLobby(core);
|
|
3724
4572
|
},
|
|
4573
|
+
// ---- M6 lane N: replays ----
|
|
4574
|
+
// D81: one getter, and deliberately only one. Everything replays do — the ring, the
|
|
4575
|
+
// eviction invariant, the recorder view, the clip assembly, the cooldown — lives in
|
|
4576
|
+
// `core/replay.ts` behind these two calls, so this file stays what it says it is: a facade
|
|
4577
|
+
// that reads through.
|
|
4578
|
+
get replay() {
|
|
4579
|
+
return {
|
|
4580
|
+
get armed() {
|
|
4581
|
+
return replayArmed(core);
|
|
4582
|
+
},
|
|
4583
|
+
clip: (seconds) => takeClip(core, seconds)
|
|
4584
|
+
};
|
|
4585
|
+
},
|
|
4586
|
+
// ---- end M6 lane N ----
|
|
3725
4587
|
alarm(name, atMs) {
|
|
3726
4588
|
if (!isAlarmName(core, name, "room.alarm")) return;
|
|
3727
4589
|
if (!Number.isFinite(atMs)) {
|
|
@@ -3804,10 +4666,10 @@ function setRole(core, clientId, role) {
|
|
|
3804
4666
|
}
|
|
3805
4667
|
if (entry.role === role) return;
|
|
3806
4668
|
const dirty = catchUpDirty(core.ext, core.plain, entry.role, role);
|
|
3807
|
-
if (!
|
|
4669
|
+
if (!isDirtyEmpty3(dirty)) {
|
|
3808
4670
|
core.send(
|
|
3809
4671
|
clientId,
|
|
3810
|
-
|
|
4672
|
+
encodeFrame4(FrameType5.DELTA, encodeDelta4(core.ext, core.plain, dirty, { tick: core.tick }))
|
|
3811
4673
|
);
|
|
3812
4674
|
}
|
|
3813
4675
|
entry.role = role;
|
|
@@ -3910,6 +4772,10 @@ var RoomCore = class _RoomCore {
|
|
|
3910
4772
|
};
|
|
3911
4773
|
tick = 0;
|
|
3912
4774
|
stopped = false;
|
|
4775
|
+
// ---- M6 lane K: stateful relay ----
|
|
4776
|
+
/** D77: the wall clock hold deadlines are measured against. See `RoomCoreOptions.wallNow`. */
|
|
4777
|
+
wallNow;
|
|
4778
|
+
// ---- end M6 lane K ----
|
|
3913
4779
|
/**
|
|
3914
4780
|
* Called for every handler throw `tryRun` swallows, before it is logged. A test harness sets
|
|
3915
4781
|
* this so a room that breaks fails the test that broke it (bug 2): the runtime's job is to
|
|
@@ -3986,6 +4852,9 @@ var RoomCore = class _RoomCore {
|
|
|
3986
4852
|
const historySetup = this.physics ? historyOf(definition.config.physics) : void 0;
|
|
3987
4853
|
this.rewindState = historySetup === void 0 ? void 0 : new RewindState(historySetup);
|
|
3988
4854
|
this.subscribeDeclaredChannels();
|
|
4855
|
+
installCaps(self, options.caps ?? {});
|
|
4856
|
+
this.wallNow = options.wallNow ?? (() => Date.now());
|
|
4857
|
+
restoreHolds(self, restored?.holds);
|
|
3989
4858
|
if (restored) {
|
|
3990
4859
|
const onWake = definition.config.onWake;
|
|
3991
4860
|
if (onWake) this.guard("onWake", () => onWake(this.state, this.room));
|
|
@@ -4326,8 +5195,13 @@ var RoomCore = class _RoomCore {
|
|
|
4326
5195
|
mode: this.mode,
|
|
4327
5196
|
schemaHash8: this.ext.hash8
|
|
4328
5197
|
},
|
|
4329
|
-
|
|
4330
|
-
physics
|
|
5198
|
+
encodeSnapshot4(this.ext, this.plain, { tick: this.tick }),
|
|
5199
|
+
physics,
|
|
5200
|
+
// ---- M6 lane K: stateful relay ----
|
|
5201
|
+
// D77: hold deadlines are state, so they go where the state goes. `undefined` for a room
|
|
5202
|
+
// holding nothing, which keeps its blob byte-identical to the one it wrote before D77.
|
|
5203
|
+
serializeHolds(this.internals)
|
|
5204
|
+
// ---- end M6 lane K ----
|
|
4331
5205
|
);
|
|
4332
5206
|
}
|
|
4333
5207
|
serialize() {
|
|
@@ -4402,6 +5276,18 @@ var RoomCore = class _RoomCore {
|
|
|
4402
5276
|
// a key join `ctx.playerId` is exactly as durable as the resume token and no more. A JWT
|
|
4403
5277
|
// join (D27, week 13) passes the verified `<iss>:<sub>` in `JoinOptions.playerId` instead,
|
|
4404
5278
|
// and nothing else here changes.
|
|
5279
|
+
//
|
|
5280
|
+
// ---- M6 lane K: stateful relay ----
|
|
5281
|
+
// An existing entry keeps the `playerId` it was created with: this is `existing ?? {...}`,
|
|
5282
|
+
// so a rejoin never overwrites it. That is deliberate and it is the safe direction — a
|
|
5283
|
+
// session's identity cannot be changed by anything a later HELLO on the same seat says. It
|
|
5284
|
+
// has one visible consequence, and D77's holds are where it shows: a client that joins
|
|
5285
|
+
// anonymously and presents an assertion later in the same session keeps the resume token's
|
|
5286
|
+
// client id as its `playerId`, so its holds stay bound to the token rather than moving to
|
|
5287
|
+
// the verified subject. Its next fresh join binds them to the subject. Said in
|
|
5288
|
+
// `authority-models.md` too, because a developer meets it as "my holds did not follow me to
|
|
5289
|
+
// the other tab" rather than as a line in this file.
|
|
5290
|
+
// ---- end M6 lane K ----
|
|
4405
5291
|
playerId: options.playerId ?? clientId,
|
|
4406
5292
|
npc: options.npc === true,
|
|
4407
5293
|
role,
|
|
@@ -4419,6 +5305,8 @@ var RoomCore = class _RoomCore {
|
|
|
4419
5305
|
entry.connected = true;
|
|
4420
5306
|
this.clients.set(clientId, entry);
|
|
4421
5307
|
this.invalidateClients();
|
|
5308
|
+
reclaimHolds(this.internals, clientId, entry.playerId);
|
|
5309
|
+
addPerPlayerRecords(this.internals, clientId);
|
|
4422
5310
|
const onJoin = this.definition.config.onJoin;
|
|
4423
5311
|
if (onJoin) {
|
|
4424
5312
|
const ctx = this.ctxFor(clientId, reconnecting);
|
|
@@ -4452,7 +5340,7 @@ var RoomCore = class _RoomCore {
|
|
|
4452
5340
|
/** Event mode only: presence/lifecycle changes are their own event. No-op in tick mode. */
|
|
4453
5341
|
eventFlush() {
|
|
4454
5342
|
if (this.mode !== "event" || this.stopped) return;
|
|
4455
|
-
if (
|
|
5343
|
+
if (isDirtyEmpty4(this.tracked.dirty)) return;
|
|
4456
5344
|
this.tick++;
|
|
4457
5345
|
this.flush();
|
|
4458
5346
|
this.captureTimeline();
|
|
@@ -4468,6 +5356,8 @@ var RoomCore = class _RoomCore {
|
|
|
4468
5356
|
const ctx = this.ctxFor(clientId);
|
|
4469
5357
|
this.guard("onLeave", () => onLeave(this.state, ctx, reason));
|
|
4470
5358
|
}
|
|
5359
|
+
placeHolds(this.internals, clientId, entry.playerId);
|
|
5360
|
+
forgetWriter(this.internals, clientId);
|
|
4471
5361
|
this.presence.remove(clientId);
|
|
4472
5362
|
this.recordEvent("leave", clientId, reason);
|
|
4473
5363
|
rejectPendingFor(this.internals, clientId, "client disconnected");
|
|
@@ -4601,6 +5491,16 @@ var RoomCore = class _RoomCore {
|
|
|
4601
5491
|
* `receive()` accepted, plus the join snapshots this room handed the host to wrap in a
|
|
4602
5492
|
* `WELCOME` (the host builds that frame, so the room never sees it — see the docs page).
|
|
4603
5493
|
*/
|
|
5494
|
+
// ---- M6 lane K: stateful relay ----
|
|
5495
|
+
/** D77: the encoded state size, when a host set the state cap. See `caps.ts`. */
|
|
5496
|
+
stateBytes() {
|
|
5497
|
+
return stateBytesOf(this.internals);
|
|
5498
|
+
}
|
|
5499
|
+
/** D77: how many records are held for players who have left. */
|
|
5500
|
+
holdCount() {
|
|
5501
|
+
return holdCount(this.internals);
|
|
5502
|
+
}
|
|
5503
|
+
// ---- end M6 lane K ----
|
|
4604
5504
|
profile() {
|
|
4605
5505
|
return this.ledger?.snapshot();
|
|
4606
5506
|
}
|
|
@@ -4631,20 +5531,20 @@ var RoomCore = class _RoomCore {
|
|
|
4631
5531
|
return;
|
|
4632
5532
|
}
|
|
4633
5533
|
switch (type) {
|
|
4634
|
-
case
|
|
4635
|
-
case
|
|
4636
|
-
if (type ===
|
|
5534
|
+
case FrameType6.WRITE:
|
|
5535
|
+
case FrameType6.CALL: {
|
|
5536
|
+
if (type === FrameType6.WRITE) this.recordEvent("write", clientId);
|
|
4637
5537
|
if (this.mode === "tick") {
|
|
4638
5538
|
this.loop.enqueue({ clientId, type, payload: payload.slice() });
|
|
4639
5539
|
} else if (!this.loop.applyEvent({ clientId, type, payload })) {
|
|
4640
5540
|
this.badFrame(
|
|
4641
5541
|
clientId,
|
|
4642
|
-
`malformed ${type ===
|
|
5542
|
+
`malformed ${type === FrameType6.WRITE ? "WRITE" : "CALL"} payload`
|
|
4643
5543
|
);
|
|
4644
5544
|
}
|
|
4645
5545
|
return;
|
|
4646
5546
|
}
|
|
4647
|
-
case
|
|
5547
|
+
case FrameType6.REPLY: {
|
|
4648
5548
|
if (!handleReply(this.internals, clientId, payload)) {
|
|
4649
5549
|
this.badFrame(clientId, "malformed REPLY payload");
|
|
4650
5550
|
} else {
|
|
@@ -4652,7 +5552,7 @@ var RoomCore = class _RoomCore {
|
|
|
4652
5552
|
}
|
|
4653
5553
|
return;
|
|
4654
5554
|
}
|
|
4655
|
-
case
|
|
5555
|
+
case FrameType6.MSG: {
|
|
4656
5556
|
if (this.mode === "event") this.tick++;
|
|
4657
5557
|
const ok = handleMsg(this.internals, clientId, payload);
|
|
4658
5558
|
if (this.mode === "event") this.flush();
|
|
@@ -4670,6 +5570,7 @@ var RoomCore = class _RoomCore {
|
|
|
4670
5570
|
*/
|
|
4671
5571
|
flush() {
|
|
4672
5572
|
evaluateLobby(this.internals);
|
|
5573
|
+
expireHolds(this.internals);
|
|
4673
5574
|
const dirty = this.tracked.flush();
|
|
4674
5575
|
serverWinsCorrections(this.internals, dirty);
|
|
4675
5576
|
this.ledger?.newFlush();
|
|
@@ -4727,14 +5628,14 @@ var RoomCore = class _RoomCore {
|
|
|
4727
5628
|
let shared;
|
|
4728
5629
|
if (entry.pendingJoinAdds) {
|
|
4729
5630
|
const sharedDirty = sharedViewDirty(this.ext, sourceDirty, entry.role);
|
|
4730
|
-
shared =
|
|
5631
|
+
shared = isDirtyEmpty4(sharedDirty) ? null : encodeDelta5(this.ext, this.plain, sharedDirty, { tick: this.tick });
|
|
4731
5632
|
if (shared) this.stats.encodesLastFlush++;
|
|
4732
5633
|
} else {
|
|
4733
5634
|
const key = viewKeyFor(this.ext, entry.role);
|
|
4734
5635
|
let cached = byView.get(key);
|
|
4735
5636
|
if (cached === void 0) {
|
|
4736
5637
|
const sharedDirty = sharedViewDirty(this.ext, dirty, entry.role);
|
|
4737
|
-
cached =
|
|
5638
|
+
cached = isDirtyEmpty4(sharedDirty) ? null : encodeDelta5(this.ext, this.plain, sharedDirty, { tick: this.tick });
|
|
4738
5639
|
if (cached) this.stats.encodesLastFlush++;
|
|
4739
5640
|
byView.set(key, cached);
|
|
4740
5641
|
}
|
|
@@ -4743,7 +5644,7 @@ var RoomCore = class _RoomCore {
|
|
|
4743
5644
|
if (shared) {
|
|
4744
5645
|
this.send(
|
|
4745
5646
|
entry.clientId,
|
|
4746
|
-
|
|
5647
|
+
encodeFrame5(FrameType6.DELTA, shared),
|
|
4747
5648
|
this.ledger ? { shared } : void 0
|
|
4748
5649
|
);
|
|
4749
5650
|
}
|
|
@@ -4754,7 +5655,7 @@ var RoomCore = class _RoomCore {
|
|
|
4754
5655
|
memberships
|
|
4755
5656
|
);
|
|
4756
5657
|
const encodeStarted = performance.now();
|
|
4757
|
-
delta =
|
|
5658
|
+
delta = isDirtyEmpty4(aoiDirty) ? null : encodeDelta5(this.ext, this.plain, aoiDirty, { tick: this.tick });
|
|
4758
5659
|
this.stats.aoiEncodeMs += performance.now() - encodeStarted;
|
|
4759
5660
|
if (delta) {
|
|
4760
5661
|
this.stats.encodesLastFlush++;
|
|
@@ -4785,7 +5686,7 @@ var RoomCore = class _RoomCore {
|
|
|
4785
5686
|
if (delta) {
|
|
4786
5687
|
this.send(
|
|
4787
5688
|
entry.clientId,
|
|
4788
|
-
|
|
5689
|
+
encodeFrame5(FrameType6.DELTA, delta),
|
|
4789
5690
|
this.ledger ? {
|
|
4790
5691
|
...churn ? { churn } : {},
|
|
4791
5692
|
...spatialDescs.length > 0 ? {} : { shared: delta }
|
|
@@ -4796,6 +5697,7 @@ var RoomCore = class _RoomCore {
|
|
|
4796
5697
|
entry.correction = void 0;
|
|
4797
5698
|
entry.accepted.clear();
|
|
4798
5699
|
}
|
|
5700
|
+
recordFlush(this.internals, dirty, byView);
|
|
4799
5701
|
}
|
|
4800
5702
|
};
|
|
4801
5703
|
function encodeSection(physics) {
|
|
@@ -4811,6 +5713,11 @@ function encodeSection(physics) {
|
|
|
4811
5713
|
export {
|
|
4812
5714
|
HOST_CALL_TIMEOUT_MS,
|
|
4813
5715
|
RoomFullError,
|
|
5716
|
+
RELAY_STATE_BYTES,
|
|
5717
|
+
RELAY_RECORDS_PER_COLLECTION,
|
|
5718
|
+
RELAY_WRITES_PER_SECOND,
|
|
5719
|
+
DEFAULT_RECORDS_PER_COLLECTION,
|
|
5720
|
+
RELAY_WRITE_CAPS,
|
|
4814
5721
|
EVENT_RING_SIZE,
|
|
4815
5722
|
inspectState,
|
|
4816
5723
|
RPC_TIMEOUT_MS,
|