@irtio/runtime 4.7.0 → 4.8.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-RLY4VAMM.js → chunk-53X6AWQZ.js} +278 -97
- package/dist/{chunk-NSUQKWYO.js → chunk-GDXHCAD6.js} +3 -3
- package/dist/{contract-CcZadpC6.d.ts → contract-BQDN3oje.d.ts} +28 -13
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/{room-BW2ZSX_k.d.ts → room-BVZiPb6s.d.ts} +10 -10
- package/dist/test/index.d.ts +5 -5
- package/dist/test/index.js +2 -2
- package/dist/worker/index.d.ts +29 -8
- package/dist/worker/index.js +18 -4
- package/package.json +7 -4
|
@@ -31,6 +31,7 @@ var RELAY_STATE_BYTES = 512 * 1024;
|
|
|
31
31
|
var RELAY_RECORDS_PER_COLLECTION = 1024;
|
|
32
32
|
var RELAY_WRITES_PER_SECOND = 35;
|
|
33
33
|
var DEFAULT_RECORDS_PER_COLLECTION = RELAY_RECORDS_PER_COLLECTION;
|
|
34
|
+
var MAX_WRITE_OPS = 4 * DEFAULT_RECORDS_PER_COLLECTION;
|
|
34
35
|
var RELAY_WRITE_CAPS = {
|
|
35
36
|
stateBytes: RELAY_STATE_BYTES,
|
|
36
37
|
recordsPerCollection: RELAY_RECORDS_PER_COLLECTION,
|
|
@@ -67,6 +68,8 @@ function maxTypeBytes(t) {
|
|
|
67
68
|
return optional + 5 + t.max * 4;
|
|
68
69
|
case "ref":
|
|
69
70
|
return optional + 5 + 64 * 4;
|
|
71
|
+
case "bytes":
|
|
72
|
+
return optional + t.n;
|
|
70
73
|
case "list":
|
|
71
74
|
return optional + 5 + t.max * maxTypeBytes(t.item);
|
|
72
75
|
case "struct": {
|
|
@@ -197,6 +200,7 @@ function forgetWriter(core, clientId) {
|
|
|
197
200
|
import { ByteReader, ByteWriter } from "@irtio/schema";
|
|
198
201
|
|
|
199
202
|
// src/core/internals.ts
|
|
203
|
+
import { bytesEqual } from "@irtio/schema";
|
|
200
204
|
function collectionDescOf(ext, name) {
|
|
201
205
|
return ext.collections.find((c) => c.name === name);
|
|
202
206
|
}
|
|
@@ -211,6 +215,9 @@ function isPlainObject(v) {
|
|
|
211
215
|
}
|
|
212
216
|
function deepEqual(a, b) {
|
|
213
217
|
if (Object.is(a, b)) return true;
|
|
218
|
+
if (a instanceof Uint8Array || b instanceof Uint8Array) {
|
|
219
|
+
return a instanceof Uint8Array && b instanceof Uint8Array && bytesEqual(a, b);
|
|
220
|
+
}
|
|
214
221
|
if (Array.isArray(a) || Array.isArray(b)) {
|
|
215
222
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
216
223
|
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
|
|
@@ -624,7 +631,7 @@ var PhysicsRuntime = class {
|
|
|
624
631
|
// Bodies
|
|
625
632
|
// -------------------------------------------------------------------------
|
|
626
633
|
/**
|
|
627
|
-
*
|
|
634
|
+
* Every teardown path goes through here rather than calling `removeRigidBody`
|
|
628
635
|
* directly.
|
|
629
636
|
*
|
|
630
637
|
* A sleeping body is parked in an island wired through the contact graph. Pulling a body out
|
|
@@ -1053,7 +1060,11 @@ function channelValue(channel, t, r, v, w) {
|
|
|
1053
1060
|
}
|
|
1054
1061
|
|
|
1055
1062
|
// src/core/inspect.ts
|
|
1056
|
-
import {
|
|
1063
|
+
import {
|
|
1064
|
+
EntityCollection,
|
|
1065
|
+
bytesToBase64,
|
|
1066
|
+
containsBytes
|
|
1067
|
+
} from "@irtio/schema";
|
|
1057
1068
|
var EVENT_RING_SIZE = 100;
|
|
1058
1069
|
var EventRing = class {
|
|
1059
1070
|
items = [];
|
|
@@ -1069,16 +1080,35 @@ function inspectState(schema, plain) {
|
|
|
1069
1080
|
const out = {};
|
|
1070
1081
|
for (const c of schema.collections) {
|
|
1071
1082
|
const v = plain[c.name];
|
|
1083
|
+
const hasBytes = c.fields.some((f) => containsBytes(f.type));
|
|
1084
|
+
const show = (value) => hasBytes ? inspectRecord(c, value) : value;
|
|
1072
1085
|
if (v instanceof EntityCollection) {
|
|
1073
1086
|
const recs = {};
|
|
1074
|
-
for (const [id, value] of v) recs[id] = { owner: v.ownerOf(id), value };
|
|
1087
|
+
for (const [id, value] of v) recs[id] = { owner: v.ownerOf(id), value: show(value) };
|
|
1075
1088
|
out[c.name] = recs;
|
|
1076
1089
|
} else {
|
|
1077
|
-
out[c.name] = v;
|
|
1090
|
+
out[c.name] = show(v);
|
|
1078
1091
|
}
|
|
1079
1092
|
}
|
|
1080
1093
|
return out;
|
|
1081
1094
|
}
|
|
1095
|
+
function inspectRecord(c, value) {
|
|
1096
|
+
if (value === null || typeof value !== "object") return value;
|
|
1097
|
+
const rec = value;
|
|
1098
|
+
const out = {};
|
|
1099
|
+
for (const f of c.fields) out[f.name] = inspectValue(f.type, rec[f.name]);
|
|
1100
|
+
return out;
|
|
1101
|
+
}
|
|
1102
|
+
function inspectValue(t, value) {
|
|
1103
|
+
if (value instanceof Uint8Array) return bytesToBase64(value);
|
|
1104
|
+
if (t.kind === "struct" && value !== null && typeof value === "object") {
|
|
1105
|
+
const rec = value;
|
|
1106
|
+
const out = {};
|
|
1107
|
+
for (const [k, ft] of Object.entries(t.fields)) out[k] = inspectValue(ft, rec[k]);
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1110
|
+
return value;
|
|
1111
|
+
}
|
|
1082
1112
|
|
|
1083
1113
|
// src/core/rpc-trace.ts
|
|
1084
1114
|
var RPC_TRACE_SIZE = 200;
|
|
@@ -1156,7 +1186,10 @@ function cellFor(grid, value) {
|
|
|
1156
1186
|
const x = record[grid.x];
|
|
1157
1187
|
const y = record[grid.y];
|
|
1158
1188
|
if (typeof x !== "number" || typeof y !== "number") return void 0;
|
|
1159
|
-
|
|
1189
|
+
const cx = Math.floor(x / grid.cell);
|
|
1190
|
+
const cy = Math.floor(y / grid.cell);
|
|
1191
|
+
if (!Number.isSafeInteger(cx) || !Number.isSafeInteger(cy)) return void 0;
|
|
1192
|
+
return { x: cx, y: cy };
|
|
1160
1193
|
}
|
|
1161
1194
|
function cellKey(cell) {
|
|
1162
1195
|
return `${cell.x},${cell.y}`;
|
|
@@ -1189,11 +1222,27 @@ function querySpatialIndex(index, clientId, role) {
|
|
|
1189
1222
|
const anchor = index.collection.get(clientId);
|
|
1190
1223
|
const anchorCell = cellFor(grid, anchor);
|
|
1191
1224
|
if (!anchor || !anchorCell) return /* @__PURE__ */ new Set();
|
|
1225
|
+
const r = grid.radius;
|
|
1226
|
+
const ax = anchorCell.x;
|
|
1227
|
+
const ay = anchorCell.y;
|
|
1228
|
+
if (!Number.isSafeInteger(ax - r) || !Number.isSafeInteger(ax + r)) return /* @__PURE__ */ new Set();
|
|
1229
|
+
if (!Number.isSafeInteger(ay - r) || !Number.isSafeInteger(ay + r)) return /* @__PURE__ */ new Set();
|
|
1192
1230
|
const candidates = /* @__PURE__ */ new Set();
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1231
|
+
const span = 2 * r + 1;
|
|
1232
|
+
if (span * span > index.buckets.size) {
|
|
1233
|
+
for (const [key, ids] of index.buckets) {
|
|
1234
|
+
const comma = key.indexOf(",");
|
|
1235
|
+
const bx = Number(key.slice(0, comma));
|
|
1236
|
+
const by = Number(key.slice(comma + 1));
|
|
1237
|
+
if (Math.abs(bx - ax) <= r && Math.abs(by - ay) <= r)
|
|
1238
|
+
for (const id of ids) candidates.add(id);
|
|
1239
|
+
}
|
|
1240
|
+
} else {
|
|
1241
|
+
for (let i = 0; i < span; i++) {
|
|
1242
|
+
for (let j = 0; j < span; j++) {
|
|
1243
|
+
const ids = index.buckets.get(cellKey({ x: ax - r + i, y: ay - r + j }));
|
|
1244
|
+
if (ids) for (const id of ids) candidates.add(id);
|
|
1245
|
+
}
|
|
1197
1246
|
}
|
|
1198
1247
|
}
|
|
1199
1248
|
candidates.add(clientId);
|
|
@@ -1519,11 +1568,22 @@ function reclaimHolds(core, clientId, playerId) {
|
|
|
1519
1568
|
s.holds.delete(key);
|
|
1520
1569
|
const coll = plainEntity(core.plain, hold.collection);
|
|
1521
1570
|
if (!coll.has(hold.id)) continue;
|
|
1522
|
-
trackedEntity(core, hold.collection)
|
|
1571
|
+
const tracked = trackedEntity(core, hold.collection);
|
|
1572
|
+
if (hold.id !== clientId && !coll.has(clientId) && isPerPlayer(core, hold.collection)) {
|
|
1573
|
+
const record = cloneValue(coll.get(hold.id));
|
|
1574
|
+
tracked.remove(hold.id);
|
|
1575
|
+
tracked.add(clientId, record, { owner: clientId });
|
|
1576
|
+
} else {
|
|
1577
|
+
tracked.setOwner(hold.id, clientId);
|
|
1578
|
+
}
|
|
1523
1579
|
reclaimed++;
|
|
1524
1580
|
}
|
|
1525
1581
|
return reclaimed;
|
|
1526
1582
|
}
|
|
1583
|
+
function isPerPlayer(core, collection) {
|
|
1584
|
+
const c = core.ext.collections.find((x) => x.name === collection);
|
|
1585
|
+
return c?.kind === "entity" && c.perPlayer === true;
|
|
1586
|
+
}
|
|
1527
1587
|
function expireHolds(core) {
|
|
1528
1588
|
const s = states2.get(core);
|
|
1529
1589
|
if (!s || s.holds.size === 0) return;
|
|
@@ -2251,6 +2311,7 @@ import {
|
|
|
2251
2311
|
SERVER_OWNER as SERVER_OWNER3,
|
|
2252
2312
|
cloneValue as cloneValue2,
|
|
2253
2313
|
decodeDelta,
|
|
2314
|
+
defaultRecord as defaultRecord2,
|
|
2254
2315
|
defaultValue,
|
|
2255
2316
|
encodeDelta as encodeDelta2,
|
|
2256
2317
|
markField,
|
|
@@ -2337,29 +2398,60 @@ function recordOf(plain, c, id) {
|
|
|
2337
2398
|
function applyWrite(core, clientId, payload) {
|
|
2338
2399
|
let delta;
|
|
2339
2400
|
try {
|
|
2340
|
-
delta = decodeDelta(core.ext, payload);
|
|
2401
|
+
delta = decodeDelta(core.ext, payload, { maxOps: MAX_WRITE_OPS });
|
|
2341
2402
|
} catch (err) {
|
|
2342
|
-
core.log(
|
|
2403
|
+
core.log(
|
|
2404
|
+
"warn",
|
|
2405
|
+
`WRITE from ${clientId} failed to decode: ${err instanceof Error ? err.message : String(err)}`
|
|
2406
|
+
);
|
|
2343
2407
|
return { ok: false };
|
|
2344
2408
|
}
|
|
2345
2409
|
let ops = 0;
|
|
2346
2410
|
for (const dc of delta.collections) ops += dc.ops.length;
|
|
2347
|
-
|
|
2411
|
+
const admitted = admitWrite(core, clientId, core.host.now(), ops);
|
|
2348
2412
|
const judged = core.clients.get(clientId);
|
|
2349
2413
|
if (judged && delta.tick > judged.lastClientTick) {
|
|
2350
2414
|
judged.lastClientTick = delta.tick;
|
|
2351
2415
|
judged.lastAppliedTick = core.tick;
|
|
2352
2416
|
}
|
|
2353
|
-
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2417
|
+
if (!admitted) {
|
|
2418
|
+
for (const dc of delta.collections) {
|
|
2419
|
+
const c = collectionDescOf(core.ext, dc.name);
|
|
2420
|
+
if (!c) continue;
|
|
2421
|
+
for (const op of dc.ops) {
|
|
2422
|
+
if (op.op !== "update" || !recordOf(core.plain, c, op.id)) continue;
|
|
2423
|
+
correctLeaves(core, clientId, c.name, op.id, leafPaths(fieldList(c), op.mask));
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return { ok: true };
|
|
2427
|
+
}
|
|
2428
|
+
const tally = { n: 0 };
|
|
2429
|
+
frameTally = tally;
|
|
2430
|
+
try {
|
|
2431
|
+
for (const dc of delta.collections) {
|
|
2432
|
+
const c = collectionDescOf(core.ext, dc.name);
|
|
2433
|
+
if (!c) continue;
|
|
2434
|
+
for (const op of dc.ops) applyOp(core, clientId, c, op);
|
|
2435
|
+
}
|
|
2436
|
+
} finally {
|
|
2437
|
+
frameTally = void 0;
|
|
2438
|
+
if (tally.n > REJECT_LOGS_PER_FRAME)
|
|
2439
|
+
core.log(
|
|
2440
|
+
"warn",
|
|
2441
|
+
`WRITE from ${clientId}: ${tally.n - REJECT_LOGS_PER_FRAME} more rejection lines suppressed (${tally.n} in this frame)`
|
|
2442
|
+
);
|
|
2357
2443
|
}
|
|
2358
2444
|
return { ok: true };
|
|
2359
2445
|
}
|
|
2446
|
+
var REJECT_LOGS_PER_FRAME = 8;
|
|
2447
|
+
var frameTally;
|
|
2448
|
+
function logRejected(core, level, ...args) {
|
|
2449
|
+
if (frameTally && ++frameTally.n > REJECT_LOGS_PER_FRAME) return;
|
|
2450
|
+
core.log(level, ...args);
|
|
2451
|
+
}
|
|
2360
2452
|
function reject(core, clientId, c, op, reason) {
|
|
2361
2453
|
core.recordEvent("write-rejected", clientId, `${c.name}: ${reason}`);
|
|
2362
|
-
core
|
|
2454
|
+
logRejected(core, "warn", `WRITE rejected (${reason}) from ${clientId} on ${c.name}`);
|
|
2363
2455
|
if (op.op !== "update") return;
|
|
2364
2456
|
if (!recordOf(core.plain, c, op.id)) return;
|
|
2365
2457
|
correctLeaves(core, clientId, c.name, op.id, leafPaths(fieldList(c), op.mask));
|
|
@@ -2377,7 +2469,8 @@ function applyOp(core, clientId, c, op) {
|
|
|
2377
2469
|
const senderRole = core.clients.get(clientId)?.role ?? "";
|
|
2378
2470
|
if (!isVisible(c, senderRole)) {
|
|
2379
2471
|
core.recordEvent("write-rejected", clientId, `${c.name}: not visible to role`);
|
|
2380
|
-
|
|
2472
|
+
logRejected(
|
|
2473
|
+
core,
|
|
2381
2474
|
"warn",
|
|
2382
2475
|
`WRITE rejected (collection not visible to role ${JSON.stringify(senderRole)}) from ${clientId} on ${c.name}`
|
|
2383
2476
|
);
|
|
@@ -2387,7 +2480,7 @@ function applyOp(core, clientId, c, op) {
|
|
|
2387
2480
|
const coll = plainEntity(core.plain, c.name);
|
|
2388
2481
|
if (coll.has(op.id) && coll.ownerOf(op.id) !== clientId && !inClientView(core, clientId, c, op.id)) {
|
|
2389
2482
|
core.recordEvent("write-rejected", clientId, `${c.name}: ${op.id} is not in view`);
|
|
2390
|
-
core
|
|
2483
|
+
logRejected(core, "warn", `WRITE rejected (id not in view) from ${clientId} on ${c.name}`);
|
|
2391
2484
|
return;
|
|
2392
2485
|
}
|
|
2393
2486
|
}
|
|
@@ -2425,6 +2518,10 @@ function applyOp(core, clientId, c, op) {
|
|
|
2425
2518
|
reject(core, clientId, c, op, "not the owner");
|
|
2426
2519
|
return;
|
|
2427
2520
|
}
|
|
2521
|
+
if (maskHasRanges(op.mask)) {
|
|
2522
|
+
reject(core, clientId, c, op, "byte-range patches are not allowed in a WRITE");
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2428
2525
|
const fields = fieldList(c);
|
|
2429
2526
|
const growth = updateGrowth(c, leafPaths(fields, op.mask));
|
|
2430
2527
|
if (growth > 0 && !admitGrowth(core, clientId, growth)) return;
|
|
@@ -2443,7 +2540,8 @@ function applyOp(core, clientId, c, op) {
|
|
|
2443
2540
|
}
|
|
2444
2541
|
if (refused2.length > 0) {
|
|
2445
2542
|
core.recordEvent("write-rejected", clientId, `${c.name}: ${refused2.join(", ")}`);
|
|
2446
|
-
|
|
2543
|
+
logRejected(
|
|
2544
|
+
core,
|
|
2447
2545
|
"warn",
|
|
2448
2546
|
`WRITE to ${c.name}.${op.id} from ${clientId} touched fields the owner does not write: ${refused2.join(", ")}`
|
|
2449
2547
|
);
|
|
@@ -2460,7 +2558,12 @@ function applyOp(core, clientId, c, op) {
|
|
|
2460
2558
|
try {
|
|
2461
2559
|
result = normalizeRecord(c, returned ?? next);
|
|
2462
2560
|
} catch (err) {
|
|
2463
|
-
|
|
2561
|
+
logRejected(
|
|
2562
|
+
core,
|
|
2563
|
+
"error",
|
|
2564
|
+
`validate.${c.name} returned an invalid record; write rejected:`,
|
|
2565
|
+
err
|
|
2566
|
+
);
|
|
2464
2567
|
result = prev;
|
|
2465
2568
|
}
|
|
2466
2569
|
}
|
|
@@ -2496,31 +2599,58 @@ function applyAdd(core, clientId, c, op) {
|
|
|
2496
2599
|
}
|
|
2497
2600
|
if (!admitGrowth(core, clientId, maxRecordBytes(c))) return;
|
|
2498
2601
|
let value = op.value;
|
|
2499
|
-
if (c.serverFields) {
|
|
2500
|
-
const
|
|
2602
|
+
if (c.physics || c.serverFields) {
|
|
2603
|
+
const refused2 = [];
|
|
2501
2604
|
const copy = { ...value };
|
|
2502
2605
|
for (const f of c.fields) {
|
|
2503
|
-
|
|
2606
|
+
const why = ownerMayNotWrite(c, f.name);
|
|
2607
|
+
if (why === void 0) continue;
|
|
2504
2608
|
const reset = defaultValue(f.type);
|
|
2505
|
-
if (deepEqual(copy[f.name], reset)) continue;
|
|
2609
|
+
if (copy[f.name] === void 0 || deepEqual(copy[f.name], reset)) continue;
|
|
2506
2610
|
copy[f.name] = reset;
|
|
2507
|
-
|
|
2611
|
+
refused2.push(why);
|
|
2508
2612
|
}
|
|
2509
|
-
if (
|
|
2510
|
-
core.recordEvent(
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
`${c.name}: ${stripped.join(", ")} (serverField)`
|
|
2514
|
-
);
|
|
2515
|
-
core.log(
|
|
2613
|
+
if (refused2.length > 0) {
|
|
2614
|
+
core.recordEvent("write-rejected", clientId, `${c.name}: ${refused2.join(", ")}`);
|
|
2615
|
+
logRejected(
|
|
2616
|
+
core,
|
|
2516
2617
|
"warn",
|
|
2517
|
-
`add to ${c.name}.${op.id} from ${clientId} set server-only fields; reset to their defaults: ${
|
|
2618
|
+
`add to ${c.name}.${op.id} from ${clientId} set server-only fields; reset to their defaults: ${refused2.join(", ")}`
|
|
2518
2619
|
);
|
|
2519
2620
|
}
|
|
2520
2621
|
value = copy;
|
|
2521
2622
|
}
|
|
2623
|
+
let record;
|
|
2624
|
+
try {
|
|
2625
|
+
record = normalizeRecord(c, value);
|
|
2626
|
+
} catch (err) {
|
|
2627
|
+
reject(core, clientId, c, op, `add rejected: ${String(err)}`);
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
const validator = core.definition.config.validate?.[c.name];
|
|
2631
|
+
if (validator) {
|
|
2632
|
+
const prev = defaultRecord2(c);
|
|
2633
|
+
const ctx = core.ctxFor(clientId);
|
|
2634
|
+
const ran = core.tryRun(`validate.${c.name}`, () => validator(prev, record, ctx));
|
|
2635
|
+
if (!ran.ok) {
|
|
2636
|
+
reject(core, clientId, c, op, `add rejected by validate.${c.name}`);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
try {
|
|
2640
|
+
record = normalizeRecord(c, ran.value ?? record);
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
reject(
|
|
2643
|
+
core,
|
|
2644
|
+
clientId,
|
|
2645
|
+
c,
|
|
2646
|
+
op,
|
|
2647
|
+
`validate.${c.name} returned an invalid record: ${String(err)}`
|
|
2648
|
+
);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2522
2652
|
try {
|
|
2523
|
-
trackedEntity(core, c.name).add(op.id,
|
|
2653
|
+
trackedEntity(core, c.name).add(op.id, record, { owner: clientId });
|
|
2524
2654
|
} catch (err) {
|
|
2525
2655
|
reject(core, clientId, c, op, `add rejected: ${String(err)}`);
|
|
2526
2656
|
return;
|
|
@@ -2550,6 +2680,11 @@ function updateGrowth(c, leaves) {
|
|
|
2550
2680
|
}
|
|
2551
2681
|
return growth;
|
|
2552
2682
|
}
|
|
2683
|
+
function maskHasRanges(mask) {
|
|
2684
|
+
if (mask.ranges && mask.ranges.size > 0) return true;
|
|
2685
|
+
for (const n of mask.nested.values()) if (maskHasRanges(n)) return true;
|
|
2686
|
+
return false;
|
|
2687
|
+
}
|
|
2553
2688
|
function isFixedWidth(t) {
|
|
2554
2689
|
switch (t.kind) {
|
|
2555
2690
|
case "bool":
|
|
@@ -2560,6 +2695,7 @@ function isFixedWidth(t) {
|
|
|
2560
2695
|
case "f32":
|
|
2561
2696
|
case "f64":
|
|
2562
2697
|
case "enum":
|
|
2698
|
+
case "bytes":
|
|
2563
2699
|
return t.opt !== true;
|
|
2564
2700
|
case "struct":
|
|
2565
2701
|
return Object.values(t.fields).every(isFixedWidth) && t.opt !== true;
|
|
@@ -2611,6 +2747,7 @@ function encodeCorrection(core, dirty) {
|
|
|
2611
2747
|
var MAX_CATCHUP = 5;
|
|
2612
2748
|
var CRASH_AFTER_THROWS = 3;
|
|
2613
2749
|
var TIMER_SLEEP_GUARD_MS = 250;
|
|
2750
|
+
var MAX_HOST_TIMER_MS = 2 ** 31 - 1;
|
|
2614
2751
|
var Loop = class {
|
|
2615
2752
|
constructor(core) {
|
|
2616
2753
|
this.core = core;
|
|
@@ -2635,7 +2772,7 @@ var Loop = class {
|
|
|
2635
2772
|
* Once, deliberately. A `setInterval(100)` in an otherwise silent room must not be able to pin
|
|
2636
2773
|
* the room resident forever — pending timers are explicitly *not* a reason to stay awake. A
|
|
2637
2774
|
* one-shot deadline is handed to the supervisor, which wakes the room at it; an interval is
|
|
2638
|
-
* simply dropped, with a warning
|
|
2775
|
+
* simply dropped, with a warning. Reset by real activity.
|
|
2639
2776
|
*/
|
|
2640
2777
|
timerDeferred = false;
|
|
2641
2778
|
get intervalMs() {
|
|
@@ -2869,15 +3006,24 @@ var Loop = class {
|
|
|
2869
3006
|
// -------------------------------------------------------------------------
|
|
2870
3007
|
setTimer(ms, fn, repeat) {
|
|
2871
3008
|
const id = this.nextTimerId++;
|
|
2872
|
-
|
|
3009
|
+
if (!Number.isFinite(ms)) {
|
|
3010
|
+
const where = repeat ? "room.setInterval" : "room.setTimeout";
|
|
3011
|
+
this.core.log("warn", `${where}: ${ms} is not a finite delay; the timer was not armed`);
|
|
3012
|
+
return id;
|
|
3013
|
+
}
|
|
3014
|
+
const periodMs = Math.max(0, ms);
|
|
3015
|
+
const periodTicks = Math.max(
|
|
3016
|
+
1,
|
|
3017
|
+
Math.ceil(periodMs * this.core.definition.config.tickRate / 1e3)
|
|
3018
|
+
);
|
|
2873
3019
|
const timer = {
|
|
2874
3020
|
id,
|
|
2875
3021
|
fn,
|
|
2876
3022
|
repeat,
|
|
2877
3023
|
periodTicks,
|
|
2878
|
-
periodMs
|
|
3024
|
+
periodMs,
|
|
2879
3025
|
dueTick: this.core.tick + periodTicks,
|
|
2880
|
-
dueAtMs: this.core.host.now() +
|
|
3026
|
+
dueAtMs: this.core.host.now() + periodMs,
|
|
2881
3027
|
handle: void 0
|
|
2882
3028
|
};
|
|
2883
3029
|
this.timers.set(id, timer);
|
|
@@ -2885,18 +3031,27 @@ var Loop = class {
|
|
|
2885
3031
|
return id;
|
|
2886
3032
|
}
|
|
2887
3033
|
armHostTimer(timer) {
|
|
2888
|
-
timer.
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
this.
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3034
|
+
const remaining = Math.max(0, timer.dueAtMs - this.core.host.now());
|
|
3035
|
+
const capped = remaining > MAX_HOST_TIMER_MS;
|
|
3036
|
+
timer.handle = this.core.host.setTimeout(
|
|
3037
|
+
() => {
|
|
3038
|
+
timer.handle = void 0;
|
|
3039
|
+
if (this.core.stopped) return;
|
|
3040
|
+
if (!this.timers.has(timer.id)) return;
|
|
3041
|
+
if (capped) {
|
|
3042
|
+
this.armHostTimer(timer);
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
if (timer.repeat) {
|
|
3046
|
+
timer.dueAtMs = this.core.host.now() + timer.periodMs;
|
|
3047
|
+
this.armHostTimer(timer);
|
|
3048
|
+
} else this.timers.delete(timer.id);
|
|
3049
|
+
this.core.tick++;
|
|
3050
|
+
this.core.guard("room timer", timer.fn);
|
|
3051
|
+
this.core.flush();
|
|
3052
|
+
},
|
|
3053
|
+
capped ? MAX_HOST_TIMER_MS : remaining
|
|
3054
|
+
);
|
|
2900
3055
|
}
|
|
2901
3056
|
clearTimer(handle) {
|
|
2902
3057
|
const t = this.timers.get(handle);
|
|
@@ -2929,7 +3084,7 @@ var Loop = class {
|
|
|
2929
3084
|
* gone" into "your room is awake again by then", which is a contract room code can actually
|
|
2930
3085
|
* catch up against in `onWake`.
|
|
2931
3086
|
*
|
|
2932
|
-
*
|
|
3087
|
+
* That contract is a statement about a **deadline**, and only a `setTimeout` has
|
|
2933
3088
|
* one. A `setInterval` has a cadence, so its next due time is one period away no matter when
|
|
2934
3089
|
* you ask — including one period after the room hibernates. Arming the wake for it made an
|
|
2935
3090
|
* interval mean "never let this room stay asleep": the room slept, the wake brought it back
|
|
@@ -3203,7 +3358,7 @@ var MatterRuntime = class {
|
|
|
3203
3358
|
// Bodies
|
|
3204
3359
|
// -------------------------------------------------------------------------
|
|
3205
3360
|
/**
|
|
3206
|
-
*
|
|
3361
|
+
* The matter2d half of waking a contact island on removal. Rapier's bindings wake a contact island themselves when a
|
|
3207
3362
|
* member is removed (`PhysicsRuntime.destroyBody` / `Rapier2dRuntime.destroyBody`); matter.js
|
|
3208
3363
|
* does not, so a body sleeping against a victim about to be removed is left sleeping and
|
|
3209
3364
|
* motionless in mid-air once the victim is gone.
|
|
@@ -3680,7 +3835,7 @@ var Rapier2dRuntime = class {
|
|
|
3680
3835
|
// Bodies
|
|
3681
3836
|
// -------------------------------------------------------------------------
|
|
3682
3837
|
/**
|
|
3683
|
-
*
|
|
3838
|
+
* The planar teardown door every removal goes through. See
|
|
3684
3839
|
* `PhysicsRuntime.destroyBody` for the argument — one narrow-phase walk of the victim's own
|
|
3685
3840
|
* contact pairs, so a body resting on what is about to vanish is awake when it does.
|
|
3686
3841
|
*/
|
|
@@ -4121,7 +4276,7 @@ import {
|
|
|
4121
4276
|
decodeSnapshot as decodeSnapshot3,
|
|
4122
4277
|
encodeDelta as encodeDelta5,
|
|
4123
4278
|
encodeSnapshot as encodeSnapshot4,
|
|
4124
|
-
filterDirty as
|
|
4279
|
+
filterDirty as filterDirty3,
|
|
4125
4280
|
isDirtyEmpty as isDirtyEmpty4,
|
|
4126
4281
|
track,
|
|
4127
4282
|
validateForDeploy
|
|
@@ -5204,6 +5359,9 @@ function lobbyConfigOf(core) {
|
|
|
5204
5359
|
if (!schemaHasLobby(core.definition.schema)) return void 0;
|
|
5205
5360
|
return declared;
|
|
5206
5361
|
}
|
|
5362
|
+
function countsAsPlayer(config, role) {
|
|
5363
|
+
return config.roles === void 0 || config.roles.includes(role);
|
|
5364
|
+
}
|
|
5207
5365
|
function stateOf5(core) {
|
|
5208
5366
|
return core.anyState[LOBBY_STATE];
|
|
5209
5367
|
}
|
|
@@ -5216,7 +5374,7 @@ function evaluateLobby(core) {
|
|
|
5216
5374
|
const policy = config.start ?? "when-full";
|
|
5217
5375
|
const present = [];
|
|
5218
5376
|
for (const [clientId, entry] of core.clients) {
|
|
5219
|
-
if (!entry.connected) continue;
|
|
5377
|
+
if (!entry.connected || !countsAsPlayer(config, entry.role)) continue;
|
|
5220
5378
|
present.push(clientId);
|
|
5221
5379
|
if (!members.has(clientId)) members.add(clientId, { ready: false }, { owner: clientId });
|
|
5222
5380
|
else if (members.ownerOf(clientId) !== clientId) members.setOwner(clientId, clientId);
|
|
@@ -5281,7 +5439,7 @@ function makeLobby(core) {
|
|
|
5281
5439
|
get ready() {
|
|
5282
5440
|
const out = [];
|
|
5283
5441
|
for (const [clientId, entry] of core.clients) {
|
|
5284
|
-
if (!entry.connected) continue;
|
|
5442
|
+
if (!entry.connected || !countsAsPlayer(config, entry.role)) continue;
|
|
5285
5443
|
out.push({
|
|
5286
5444
|
clientId,
|
|
5287
5445
|
ready: members.get(clientId)?.ready === true
|
|
@@ -5336,7 +5494,8 @@ import {
|
|
|
5336
5494
|
encodeChatMsg,
|
|
5337
5495
|
encodeFrame as encodeFrame3,
|
|
5338
5496
|
encodeMsg,
|
|
5339
|
-
isChatMsg
|
|
5497
|
+
isChatMsg,
|
|
5498
|
+
toWireTarget
|
|
5340
5499
|
} from "@irtio/protocol";
|
|
5341
5500
|
import { decodeFields as decodeFields2 } from "@irtio/schema";
|
|
5342
5501
|
function toRoomTarget(target) {
|
|
@@ -5458,22 +5617,20 @@ function handleChat(core, clientId, payload) {
|
|
|
5458
5617
|
deliver(core, chat.target, frame, clientId);
|
|
5459
5618
|
return true;
|
|
5460
5619
|
}
|
|
5461
|
-
function wireTargetOf(target) {
|
|
5462
|
-
return target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
5463
|
-
}
|
|
5464
5620
|
function sendMessage(core, target, bytes) {
|
|
5465
5621
|
const frame = encodeFrame3(
|
|
5466
5622
|
FrameType4.MSG,
|
|
5467
5623
|
encodeMsg({ target: { kind: "server" }, payload: bytes })
|
|
5468
5624
|
);
|
|
5469
|
-
deliver(core,
|
|
5625
|
+
deliver(core, toWireTarget(target, "room.send"), frame);
|
|
5470
5626
|
}
|
|
5471
|
-
function sendTypedMessage(core, index, target, payload) {
|
|
5627
|
+
function sendTypedMessage(core, index, target, payload, name = "message") {
|
|
5628
|
+
const wire = toWireTarget(target, `room.messages.${name}.send`);
|
|
5472
5629
|
const frame = encodeFrame3(
|
|
5473
5630
|
FrameType4.MSG,
|
|
5474
5631
|
encodeMsg({ target: { kind: "server" }, payload, typed: { index } })
|
|
5475
5632
|
);
|
|
5476
|
-
deliver(core,
|
|
5633
|
+
deliver(core, wire, frame);
|
|
5477
5634
|
}
|
|
5478
5635
|
|
|
5479
5636
|
// src/core/replay.ts
|
|
@@ -5489,12 +5646,14 @@ import {
|
|
|
5489
5646
|
import {
|
|
5490
5647
|
encodeDelta as encodeDelta3,
|
|
5491
5648
|
encodeSnapshot as encodeSnapshot3,
|
|
5649
|
+
filterDirty as filterDirty2,
|
|
5492
5650
|
isDirtyEmpty as isDirtyEmpty2
|
|
5493
5651
|
} from "@irtio/schema";
|
|
5494
5652
|
|
|
5495
5653
|
// src/core/replay-excerpt.ts
|
|
5496
5654
|
import {
|
|
5497
5655
|
applyDelta,
|
|
5656
|
+
copyBytes,
|
|
5498
5657
|
decodeDelta as decodeDelta2,
|
|
5499
5658
|
decodeSnapshot as decodeSnapshot2
|
|
5500
5659
|
} from "@irtio/schema";
|
|
@@ -5512,6 +5671,7 @@ var ExcerptTooLargeError = class extends Error {
|
|
|
5512
5671
|
};
|
|
5513
5672
|
function estimateValue(value) {
|
|
5514
5673
|
if (typeof value === "string") return value.length + 2;
|
|
5674
|
+
if (value instanceof Uint8Array) return value.length + 2;
|
|
5515
5675
|
if (Array.isArray(value)) {
|
|
5516
5676
|
let n = 2;
|
|
5517
5677
|
for (const v of value) n += estimateValue(v) + 1;
|
|
@@ -5527,6 +5687,7 @@ function estimateValue(value) {
|
|
|
5527
5687
|
return 8;
|
|
5528
5688
|
}
|
|
5529
5689
|
function copyValue(value) {
|
|
5690
|
+
if (value instanceof Uint8Array) return copyBytes(value);
|
|
5530
5691
|
if (Array.isArray(value)) return value.map(copyValue);
|
|
5531
5692
|
if (typeof value === "object" && value !== null) {
|
|
5532
5693
|
const out = {};
|
|
@@ -5571,14 +5732,15 @@ function wantedCollections(ext, names) {
|
|
|
5571
5732
|
const wanted = new Set(names);
|
|
5572
5733
|
return all.filter((c) => wanted.has(c.name));
|
|
5573
5734
|
}
|
|
5574
|
-
function unknownCollection(ext, options) {
|
|
5735
|
+
function unknownCollection(ext, options, recorded) {
|
|
5575
5736
|
const names = options?.collections;
|
|
5576
5737
|
if (names === void 0) return void 0;
|
|
5577
5738
|
const known = new Set(
|
|
5578
5739
|
ext.collections.filter((c) => c.kind === "entity").map((c) => c.name)
|
|
5579
5740
|
);
|
|
5580
5741
|
for (const name of names) {
|
|
5581
|
-
if (!known.has(name)) return String(name);
|
|
5742
|
+
if (!known.has(name)) return { kind: "unknown", name: String(name) };
|
|
5743
|
+
if (recorded !== void 0 && !recorded.has(name)) return { kind: "unrecorded", name };
|
|
5582
5744
|
}
|
|
5583
5745
|
return void 0;
|
|
5584
5746
|
}
|
|
@@ -5751,6 +5913,8 @@ function stateOf6(core) {
|
|
|
5751
5913
|
ttlMs,
|
|
5752
5914
|
recExpiresAtEpochS: void 0,
|
|
5753
5915
|
view: declared.view ?? "",
|
|
5916
|
+
mode: declared.omniscient === true ? "omniscient" : declared.view !== void 0 ? "role" : "public",
|
|
5917
|
+
recorded: void 0,
|
|
5754
5918
|
ring: new ReplayRing(
|
|
5755
5919
|
{
|
|
5756
5920
|
windowTicks: Math.max(1, Math.ceil(seconds * tickRate)),
|
|
@@ -5785,13 +5949,23 @@ function stateOf6(core) {
|
|
|
5785
5949
|
}
|
|
5786
5950
|
return s;
|
|
5787
5951
|
}
|
|
5788
|
-
function
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
if (
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5952
|
+
function recorderCollections(core, state) {
|
|
5953
|
+
if (state.recorded !== void 0) return state.recorded;
|
|
5954
|
+
const keep = (c) => {
|
|
5955
|
+
if (c.visibility === "server") return false;
|
|
5956
|
+
switch (state.mode) {
|
|
5957
|
+
case "omniscient":
|
|
5958
|
+
return true;
|
|
5959
|
+
case "role":
|
|
5960
|
+
return isVisible(c, state.view) && c.visibility !== "owner";
|
|
5961
|
+
default:
|
|
5962
|
+
return c.visibility !== "role" && c.visibility !== "owner";
|
|
5963
|
+
}
|
|
5964
|
+
};
|
|
5965
|
+
state.recorded = new Set(
|
|
5966
|
+
core.ext.collections.filter(keep).map((c) => c.name)
|
|
5967
|
+
);
|
|
5968
|
+
return state.recorded;
|
|
5795
5969
|
}
|
|
5796
5970
|
function hasRoleScoped(core) {
|
|
5797
5971
|
return core.ext.collections.some((c) => c.visibility === "role");
|
|
@@ -5800,28 +5974,30 @@ function hasSpatial(core) {
|
|
|
5800
5974
|
return core.ext.collections.some(isPerClient);
|
|
5801
5975
|
}
|
|
5802
5976
|
function recorderSnapshot(core, state) {
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
}
|
|
5806
|
-
return encodeViewSnapshot(
|
|
5977
|
+
const names = recorderCollections(core, state);
|
|
5978
|
+
return encodeSnapshot3(
|
|
5807
5979
|
core.ext,
|
|
5808
5980
|
core.plain,
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
omniscientMemberships(core)
|
|
5981
|
+
{ tick: core.tick },
|
|
5982
|
+
{ collections: (c) => names.has(c.name) }
|
|
5812
5983
|
);
|
|
5813
5984
|
}
|
|
5814
5985
|
function recorderDelta(core, state, dirty) {
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
}
|
|
5819
|
-
return encodeViewDelta(core.ext, core.plain, dirty, state.view, core.tick);
|
|
5986
|
+
const names = recorderCollections(core, state);
|
|
5987
|
+
const filtered = filterDirty2(dirty, (n) => names.has(n));
|
|
5988
|
+
if (isDirtyEmpty2(filtered)) return null;
|
|
5989
|
+
return encodeDelta3(core.ext, core.plain, filtered, { tick: core.tick });
|
|
5820
5990
|
}
|
|
5821
5991
|
function cacheKeyFor(core, state) {
|
|
5822
5992
|
if (hasSpatial(core)) return void 0;
|
|
5823
|
-
|
|
5824
|
-
|
|
5993
|
+
switch (state.mode) {
|
|
5994
|
+
case "public":
|
|
5995
|
+
return "all";
|
|
5996
|
+
case "role":
|
|
5997
|
+
return viewKeyFor(core.ext, state.view);
|
|
5998
|
+
default:
|
|
5999
|
+
return hasRoleScoped(core) ? void 0 : "all";
|
|
6000
|
+
}
|
|
5825
6001
|
}
|
|
5826
6002
|
function recordFlush(core, dirty, byView) {
|
|
5827
6003
|
const state = stateOf6(core);
|
|
@@ -5979,16 +6155,19 @@ function takeExcerpt(core, options) {
|
|
|
5979
6155
|
)
|
|
5980
6156
|
);
|
|
5981
6157
|
}
|
|
5982
|
-
const
|
|
5983
|
-
|
|
6158
|
+
const state = stateOf6(core);
|
|
6159
|
+
const bad = unknownCollection(core.ext, options, recorderCollections(core, state));
|
|
6160
|
+
if (bad !== void 0) {
|
|
5984
6161
|
return rejectAsEvent(
|
|
5985
6162
|
core,
|
|
5986
6163
|
new Error(
|
|
5987
|
-
`E_EXCERPT_BAD_ARGS: ${JSON.stringify(
|
|
6164
|
+
bad.kind === "unknown" ? `E_EXCERPT_BAD_ARGS: ${JSON.stringify(bad.name)} is not an entity collection here; singletons are not excerptable` : (
|
|
6165
|
+
// Short on purpose: `rejectAsEvent` clamps to 96 bytes, and the fix is the second half.
|
|
6166
|
+
`E_EXCERPT_BAD_ARGS: ${JSON.stringify(bad.name)} is not recorded (replay.view / replay.omniscient)`
|
|
6167
|
+
)
|
|
5988
6168
|
)
|
|
5989
6169
|
);
|
|
5990
6170
|
}
|
|
5991
|
-
const state = stateOf6(core);
|
|
5992
6171
|
if (state.disarmed) {
|
|
5993
6172
|
return rejectAsEvent(
|
|
5994
6173
|
core,
|
|
@@ -6429,7 +6608,7 @@ function createRoomApi(core, publicUrl) {
|
|
|
6429
6608
|
for (const desc of core.definition.schema.messages ?? []) {
|
|
6430
6609
|
messages[desc.name] = {
|
|
6431
6610
|
send(target, value) {
|
|
6432
|
-
sendTypedMessage(core, desc.index, target, encodeFields2(desc.fields, value));
|
|
6611
|
+
sendTypedMessage(core, desc.index, target, encodeFields2(desc.fields, value), desc.name);
|
|
6433
6612
|
}
|
|
6434
6613
|
};
|
|
6435
6614
|
}
|
|
@@ -6784,6 +6963,7 @@ function setRole(core, clientId, role) {
|
|
|
6784
6963
|
entry.role = role;
|
|
6785
6964
|
if (catchUp) core.send(clientId, catchUp);
|
|
6786
6965
|
core.invalidateClients();
|
|
6966
|
+
core.host.roleChanged?.(clientId, role);
|
|
6787
6967
|
}
|
|
6788
6968
|
function checkNpcConfig(core, config) {
|
|
6789
6969
|
if (typeof config !== "object" || config === null) {
|
|
@@ -7424,7 +7604,7 @@ var RoomCore = class _RoomCore {
|
|
|
7424
7604
|
* after the time it would have fired — so keep the deadline in state and catch up in `onWake`.
|
|
7425
7605
|
*
|
|
7426
7606
|
* A repeating timer is deliberately not in here: it has a cadence rather than a deadline, and a
|
|
7427
|
-
* wake armed for it can only ever hand the room back its own next period (
|
|
7607
|
+
* wake armed for it can only ever hand the room back its own next period (see
|
|
7428
7608
|
* `Loop.departureTimers` for the whole argument). The warning below is what a room holding one
|
|
7429
7609
|
* gets instead.
|
|
7430
7610
|
*
|
|
@@ -7604,7 +7784,7 @@ var RoomCore = class _RoomCore {
|
|
|
7604
7784
|
}
|
|
7605
7785
|
const presence = this.presence;
|
|
7606
7786
|
const maxClients = this.definition.config.maxClients;
|
|
7607
|
-
const resuming =
|
|
7787
|
+
const resuming = presence.has(clientId);
|
|
7608
7788
|
if (!resuming) this.admit(clientId, options);
|
|
7609
7789
|
if (!presence.has(clientId) && presence.size >= maxClients) {
|
|
7610
7790
|
throw new RoomFullError(maxClients);
|
|
@@ -7723,6 +7903,7 @@ var RoomCore = class _RoomCore {
|
|
|
7723
7903
|
this.guard("onLeave", () => onLeave(this.state, ctx, reason));
|
|
7724
7904
|
}
|
|
7725
7905
|
cancelAsksFor(this.internals, clientId);
|
|
7906
|
+
entry.connected = false;
|
|
7726
7907
|
placeHolds(this.internals, clientId, entry.playerId);
|
|
7727
7908
|
forgetWriter(this.internals, clientId);
|
|
7728
7909
|
this.presence.remove(clientId);
|
|
@@ -7804,7 +7985,7 @@ var RoomCore = class _RoomCore {
|
|
|
7804
7985
|
};
|
|
7805
7986
|
}
|
|
7806
7987
|
/**
|
|
7807
|
-
*
|
|
7988
|
+
* The host answering a `room.save()` / `room.kv.*`. The continuation runs here — its
|
|
7808
7989
|
* own event, between ticks, off the back of a host turn — and the flush afterwards is what
|
|
7809
7990
|
* makes "state mutated in a continuation is tracked normally" true rather than aspirational.
|
|
7810
7991
|
*/
|
|
@@ -8028,7 +8209,7 @@ var RoomCore = class _RoomCore {
|
|
|
8028
8209
|
const keep = visibleNames(this.ext, entry.role);
|
|
8029
8210
|
const payload = encodeCorrection(
|
|
8030
8211
|
this.internals,
|
|
8031
|
-
|
|
8212
|
+
filterDirty3(
|
|
8032
8213
|
membershipSafeDirty(this.ext, entry.correction, memberships),
|
|
8033
8214
|
(name) => keep.has(name)
|
|
8034
8215
|
)
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
parseHibernationBlob,
|
|
4
4
|
sameSchemaHash,
|
|
5
5
|
writeHibernationBlob
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-53X6AWQZ.js";
|
|
7
7
|
|
|
8
8
|
// src/migrate.ts
|
|
9
9
|
import { PRESENCE_COLLECTION, withBuiltins } from "@irtio/protocol";
|
|
@@ -146,12 +146,12 @@ function migrateHolds(bytes, target, options) {
|
|
|
146
146
|
return new TextEncoder().encode(JSON.stringify(kept));
|
|
147
147
|
}
|
|
148
148
|
function toMigrationState(schema, plain) {
|
|
149
|
-
const out =
|
|
149
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
150
150
|
for (const c of schema.collections) {
|
|
151
151
|
const held = plain[c.name];
|
|
152
152
|
if (c.kind === "entity") {
|
|
153
153
|
const coll = held;
|
|
154
|
-
const map =
|
|
154
|
+
const map = /* @__PURE__ */ Object.create(null);
|
|
155
155
|
for (const [id, rec] of coll.records) {
|
|
156
156
|
map[id] = { owner: rec.owner, value: { ...rec.value } };
|
|
157
157
|
}
|
|
@@ -168,7 +168,13 @@ interface RoomEvent {
|
|
|
168
168
|
readonly detail?: string;
|
|
169
169
|
}
|
|
170
170
|
declare const EVENT_RING_SIZE = 100;
|
|
171
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* JSON-able view of a plain state: entities as `{id: {owner, value}}`, singletons as values.
|
|
173
|
+
*
|
|
174
|
+
* A `bytes(n)` value is shown as its base64 string rather than as the `{"0": .., "1": ..}` object
|
|
175
|
+
* JSON makes of a `Uint8Array`. Only collections that declare a bytes field are copied to do that;
|
|
176
|
+
* every other value is handed back as it is (callers that keep it clone it, see `timeline.ts`).
|
|
177
|
+
*/
|
|
172
178
|
declare function inspectState(schema: AnySchema, plain: PlainState): Record<string, unknown>;
|
|
173
179
|
interface RoomInspection {
|
|
174
180
|
readonly tick: number;
|
|
@@ -177,7 +183,7 @@ interface RoomInspection {
|
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
/**
|
|
180
|
-
*
|
|
186
|
+
* The write core's caps, and the named refusals that enforce them.
|
|
181
187
|
*
|
|
182
188
|
* ## Why these are here and not in the relay host
|
|
183
189
|
*
|
|
@@ -221,7 +227,7 @@ interface RoomInspection {
|
|
|
221
227
|
* costs. A room under half the cap pays one measure per stale window and nothing else.
|
|
222
228
|
*/
|
|
223
229
|
|
|
224
|
-
/**
|
|
230
|
+
/** The relay host's cap values. Kept in one place so they move together. */
|
|
225
231
|
declare const RELAY_STATE_BYTES: number;
|
|
226
232
|
declare const RELAY_RECORDS_PER_COLLECTION = 1024;
|
|
227
233
|
declare const RELAY_WRITES_PER_SECOND = 35;
|
|
@@ -240,8 +246,7 @@ declare const RELAY_WRITE_CAPS: WriteCaps;
|
|
|
240
246
|
|
|
241
247
|
type LogLevel = 'info' | 'warn' | 'error';
|
|
242
248
|
/**
|
|
243
|
-
*
|
|
244
|
-
* operation (D25). Room handlers are synchronous and there is no way to block one on I/O, so
|
|
249
|
+
* Host-side async work a room asked for — a save generation or a player-KV operation. Room handlers are synchronous and there is no way to block one on I/O, so
|
|
245
250
|
* every one of these is started by `RoomHost.hostCall` and *finished* by the host calling
|
|
246
251
|
* `RoomCoreApi.completeHostCall` — the same way an inbound frame arrives. That is what makes
|
|
247
252
|
* "the continuation runs as its own event between ticks" a structural property of the seam
|
|
@@ -274,7 +279,7 @@ type HostCall = {
|
|
|
274
279
|
readonly board: string;
|
|
275
280
|
readonly playerId: string;
|
|
276
281
|
readonly score: number;
|
|
277
|
-
/**
|
|
282
|
+
/** The cohort of a bucketed board. Absent on every board that declares
|
|
278
283
|
* no buckets, which is every board that existed before M6. There is deliberately no
|
|
279
284
|
* `period` beside it: rotation is control's clock, never the room's. */
|
|
280
285
|
readonly bucket?: string;
|
|
@@ -413,7 +418,7 @@ interface RoomHost {
|
|
|
413
418
|
/** N consecutive tick throws; the runtime does not recover itself. */
|
|
414
419
|
crashed(reason: string): void;
|
|
415
420
|
/**
|
|
416
|
-
*
|
|
421
|
+
* Start host-side async work (a save or a KV operation). The host **must** deliver the
|
|
417
422
|
* outcome by calling `RoomCoreApi.completeHostCall(reqId, result)` from its own event loop
|
|
418
423
|
* turn, never synchronously from inside this call — a handler is on the stack.
|
|
419
424
|
*/
|
|
@@ -435,6 +440,16 @@ interface RoomHost {
|
|
|
435
440
|
* A host with no room list (the in-process test harness) records it and does nothing else.
|
|
436
441
|
*/
|
|
437
442
|
setBackfill(open: boolean): void;
|
|
443
|
+
/**
|
|
444
|
+
* #217: `room.setRole` committed `role` for `clientId`. Fire-and-forget host state, in the
|
|
445
|
+
* style of `setBackfill`: the supervisor mirrors it onto the session so a rejoin (reconnect,
|
|
446
|
+
* wake, crash restart, migration) re-asserts the room's latest grant, not the join-time one.
|
|
447
|
+
* Only called after the role is actually applied, and never for a no-op or refused call.
|
|
448
|
+
*
|
|
449
|
+
* Optional: a host with no sessions to mirror onto (the in-process harness, the relay host)
|
|
450
|
+
* need not implement it.
|
|
451
|
+
*/
|
|
452
|
+
roleChanged?(clientId: string, role: string): void;
|
|
438
453
|
/**
|
|
439
454
|
* D75: `room.lobby.setPublic(v)`, and the terminal deregistration a game start performs.
|
|
440
455
|
*
|
|
@@ -512,7 +527,7 @@ interface RoomCoreOptions {
|
|
|
512
527
|
*/
|
|
513
528
|
readonly traceRpc?: boolean;
|
|
514
529
|
/**
|
|
515
|
-
*
|
|
530
|
+
* The write core's caps — encoded state bytes, records per collection, writes per
|
|
516
531
|
* client per second. A host that sets none gets the record cap and nothing else, which is what
|
|
517
532
|
* every coded room in its own VM wants: it already has a memory ceiling and a tick budget. The
|
|
518
533
|
* relay host sets all three, because it serves many tenants from one process.
|
|
@@ -553,8 +568,8 @@ interface JoinOptions {
|
|
|
553
568
|
/**
|
|
554
569
|
* D25: the identity `ctx.playerId` reports and `room.kv` is meant to be keyed by. Defaults to
|
|
555
570
|
* `clientId`, which — because a resume token carries the client id across a reconnect — is
|
|
556
|
-
* exactly the resume-token identity, and no more stable than that token.
|
|
557
|
-
*
|
|
571
|
+
* exactly the resume-token identity, and no more stable than that token. A JWT join passes
|
|
572
|
+
* the verified subject here instead without anything else changing.
|
|
558
573
|
*/
|
|
559
574
|
readonly playerId?: string;
|
|
560
575
|
/** D44: this join is a scripted NPC's loopback session. */
|
|
@@ -588,7 +603,7 @@ declare class RoomFullError extends Error {
|
|
|
588
603
|
constructor(maxClients: number);
|
|
589
604
|
}
|
|
590
605
|
/**
|
|
591
|
-
*
|
|
606
|
+
* This join was refused before it had a seat.
|
|
592
607
|
*
|
|
593
608
|
* Thrown by `RoomCore.join` for all four of the room's own refusals — a subject inside a
|
|
594
609
|
* `room.ban` cooldown, an `onAdmit` that called `ctx.deny`, an `onAdmit` that never answered
|
|
@@ -703,7 +718,7 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
|
|
|
703
718
|
* `host.now()`'s clock, or `undefined` if there was none. The worker translates it to wall
|
|
704
719
|
* clock and hands it to the supervisor, which wakes the room at it — a `room.setTimeout` loses
|
|
705
720
|
* its callback to a hibernation but no longer loses the room's chance to act on the deadline.
|
|
706
|
-
* A `room.setInterval` is not a deadline and is deliberately not reported
|
|
721
|
+
* A `room.setInterval` is not a deadline and is deliberately not reported; the
|
|
707
722
|
* departure logs a warning for one instead. `undefined` until the first `serialize()`, and
|
|
708
723
|
* never touched by `snapshot()`.
|
|
709
724
|
*/
|
|
@@ -738,7 +753,7 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
|
|
|
738
753
|
*/
|
|
739
754
|
rpcTrace(): RpcTraceDump | undefined;
|
|
740
755
|
/**
|
|
741
|
-
*
|
|
756
|
+
* Settles the promise `hostCall(reqId, …)` returned. The host calls this from its own
|
|
742
757
|
* turn; the runtime runs the continuation as a discrete event and flushes afterwards, so state
|
|
743
758
|
* a continuation mutated goes out exactly like state a handler mutated.
|
|
744
759
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AdmissionError, D as DEFAULT_RECORDS_PER_COLLECTION, a as DEFAULT_TIMELINE_MAX_RECORDS, b as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, c as HostCall, d as HostCallResult, J as JoinOptions, e as JoinResult, L as LogLevel, P as PHYSICS_STEP_EMA_ALPHA, R as RELAY_RECORDS_PER_COLLECTION, f as RELAY_STATE_BYTES, g as RELAY_WRITES_PER_SECOND, h as RELAY_WRITE_CAPS, i as RPC_TRACE_SIZE, j as RPC_TRACE_SUMMARY_MAX, k as RoomCoreApi, l as RoomCoreOptions, m as RoomEvent, n as RoomEventKind, o as RoomFullError, p as RoomHost, q as RoomInspection, r as RoomStats, s as RpcOutcome, t as RpcTraceDump, u as RpcTraceEntry, v as RpcTraceRing, S as StampLag, T as TimelineDump, w as TimelineFrame, x as TimelineRecorder, y as TimelineRecorderOptions, W as WriteCaps, z as inspectState, B as summarizeRpcResult } from './contract-
|
|
2
|
-
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-
|
|
1
|
+
export { A as AdmissionError, D as DEFAULT_RECORDS_PER_COLLECTION, a as DEFAULT_TIMELINE_MAX_RECORDS, b as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, c as HostCall, d as HostCallResult, J as JoinOptions, e as JoinResult, L as LogLevel, P as PHYSICS_STEP_EMA_ALPHA, R as RELAY_RECORDS_PER_COLLECTION, f as RELAY_STATE_BYTES, g as RELAY_WRITES_PER_SECOND, h as RELAY_WRITE_CAPS, i as RPC_TRACE_SIZE, j as RPC_TRACE_SUMMARY_MAX, k as RoomCoreApi, l as RoomCoreOptions, m as RoomEvent, n as RoomEventKind, o as RoomFullError, p as RoomHost, q as RoomInspection, r as RoomStats, s as RpcOutcome, t as RpcTraceDump, u as RpcTraceEntry, v as RpcTraceRing, S as StampLag, T as TimelineDump, w as TimelineFrame, x as TimelineRecorder, y as TimelineRecorderOptions, W as WriteCaps, z as inspectState, B as summarizeRpcResult } from './contract-BQDN3oje.js';
|
|
2
|
+
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-BVZiPb6s.js';
|
|
3
3
|
import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
|
|
4
4
|
import '@irtio/protocol';
|
|
5
5
|
import '@irtio/server';
|
|
@@ -137,7 +137,7 @@ interface ParsedSnapshot extends SnapshotHeader {
|
|
|
137
137
|
readonly version: number;
|
|
138
138
|
/** The encoded physics section, or `undefined` for a v1 blob / a room with no world. */
|
|
139
139
|
readonly physics: Uint8Array | undefined;
|
|
140
|
-
/**
|
|
140
|
+
/** The hold deadlines section, or `undefined` when the room held nothing. */
|
|
141
141
|
readonly holds: Uint8Array | undefined;
|
|
142
142
|
/** The codec snapshot, encoded under `withBuiltins(schema)`. */
|
|
143
143
|
readonly snapshot: Uint8Array;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
fromMigrationState,
|
|
3
3
|
migrateSnapshot,
|
|
4
4
|
toMigrationState
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GDXHCAD6.js";
|
|
6
6
|
import {
|
|
7
7
|
AdmissionError,
|
|
8
8
|
CRASH_AFTER_THROWS,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
visibleNames,
|
|
60
60
|
visibleTo,
|
|
61
61
|
writeHibernationBlob
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-53X6AWQZ.js";
|
|
63
63
|
export {
|
|
64
64
|
AdmissionError,
|
|
65
65
|
CRASH_AFTER_THROWS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PlatformNotice, AttributeOptions, ProfileSnapshot } from '@irtio/protocol';
|
|
2
2
|
import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
|
|
3
3
|
import { RoomDefinition, RoomMode, Room, RewindView, Ctx, CustomStepper, CustomPose, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, Rapier2dModule, Rapier2dWorld, Rapier2dRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
|
|
4
|
-
import { p as RoomHost, r as RoomStats, u as RpcTraceEntry, L as LogLevel, k as RoomCoreApi, l as RoomCoreOptions, n as RoomEventKind, y as TimelineRecorderOptions, T as TimelineDump, t as RpcTraceDump, q as RoomInspection, J as JoinOptions, e as JoinResult, d as HostCallResult } from './contract-
|
|
4
|
+
import { p as RoomHost, r as RoomStats, u as RpcTraceEntry, L as LogLevel, k as RoomCoreApi, l as RoomCoreOptions, n as RoomEventKind, y as TimelineRecorderOptions, T as TimelineDump, t as RpcTraceDump, q as RoomInspection, J as JoinOptions, e as JoinResult, d as HostCallResult } from './contract-BQDN3oje.js';
|
|
5
5
|
|
|
6
6
|
declare class Mulberry32 {
|
|
7
7
|
/** Current internal state (u32). Survives hibernation. */
|
|
@@ -30,7 +30,7 @@ interface ClientEntry {
|
|
|
30
30
|
*/
|
|
31
31
|
readonly playerId: string;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* The verified identity subject behind this session, or `undefined` for a key join.
|
|
34
34
|
* Set once at the first join beside `playerId` and kept for the same reason: what a later HELLO
|
|
35
35
|
* on the same seat claims must not be able to change who the seat belongs to. `room.ban` reads
|
|
36
36
|
* it, and reading `undefined` is what makes "a key join cannot be banned durably" a fact of the
|
|
@@ -201,7 +201,7 @@ interface LoopApi {
|
|
|
201
201
|
/**
|
|
202
202
|
* The departure's read, taken at `serialize()` before the timers are cleared: the earliest
|
|
203
203
|
* **one-shot** deadline, which the supervisor guarantees the room is awake again by, and the
|
|
204
|
-
* number of repeating timers dropped with no such guarantee
|
|
204
|
+
* number of repeating timers dropped with no such guarantee. See
|
|
205
205
|
* `Loop.departureTimers`.
|
|
206
206
|
*/
|
|
207
207
|
departureTimers(): {
|
|
@@ -442,7 +442,7 @@ declare class Loop implements LoopApi {
|
|
|
442
442
|
* Once, deliberately. A `setInterval(100)` in an otherwise silent room must not be able to pin
|
|
443
443
|
* the room resident forever — pending timers are explicitly *not* a reason to stay awake. A
|
|
444
444
|
* one-shot deadline is handed to the supervisor, which wakes the room at it; an interval is
|
|
445
|
-
* simply dropped, with a warning
|
|
445
|
+
* simply dropped, with a warning. Reset by real activity.
|
|
446
446
|
*/
|
|
447
447
|
private timerDeferred;
|
|
448
448
|
constructor(core: RoomInternals);
|
|
@@ -510,7 +510,7 @@ declare class Loop implements LoopApi {
|
|
|
510
510
|
* gone" into "your room is awake again by then", which is a contract room code can actually
|
|
511
511
|
* catch up against in `onWake`.
|
|
512
512
|
*
|
|
513
|
-
*
|
|
513
|
+
* That contract is a statement about a **deadline**, and only a `setTimeout` has
|
|
514
514
|
* one. A `setInterval` has a cadence, so its next due time is one period away no matter when
|
|
515
515
|
* you ask — including one period after the room hibernates. Arming the wake for it made an
|
|
516
516
|
* interval mean "never let this room stay asleep": the room slept, the wake brought it back
|
|
@@ -665,7 +665,7 @@ declare class MatterRuntime {
|
|
|
665
665
|
runSetup(room: Room): void;
|
|
666
666
|
free(): void;
|
|
667
667
|
/**
|
|
668
|
-
*
|
|
668
|
+
* The matter2d half of waking a contact island on removal. Rapier's bindings wake a contact island themselves when a
|
|
669
669
|
* member is removed (`PhysicsRuntime.destroyBody` / `Rapier2dRuntime.destroyBody`); matter.js
|
|
670
670
|
* does not, so a body sleeping against a victim about to be removed is left sleeping and
|
|
671
671
|
* motionless in mid-air once the victim is gone.
|
|
@@ -884,7 +884,7 @@ declare class PhysicsRuntime {
|
|
|
884
884
|
runSetup(room: Room): void;
|
|
885
885
|
free(): void;
|
|
886
886
|
/**
|
|
887
|
-
*
|
|
887
|
+
* Every teardown path goes through here rather than calling `removeRigidBody`
|
|
888
888
|
* directly.
|
|
889
889
|
*
|
|
890
890
|
* A sleeping body is parked in an island wired through the contact graph. Pulling a body out
|
|
@@ -1054,7 +1054,7 @@ declare class Rapier2dRuntime {
|
|
|
1054
1054
|
runSetup(room: Room): void;
|
|
1055
1055
|
free(): void;
|
|
1056
1056
|
/**
|
|
1057
|
-
*
|
|
1057
|
+
* The planar teardown door every removal goes through. See
|
|
1058
1058
|
* `PhysicsRuntime.destroyBody` for the argument — one narrow-phase walk of the victim's own
|
|
1059
1059
|
* contact pairs, so a body resting on what is about to vanish is awake when it does.
|
|
1060
1060
|
*/
|
|
@@ -1318,7 +1318,7 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
1318
1318
|
* after the time it would have fired — so keep the deadline in state and catch up in `onWake`.
|
|
1319
1319
|
*
|
|
1320
1320
|
* A repeating timer is deliberately not in here: it has a cadence rather than a deadline, and a
|
|
1321
|
-
* wake armed for it can only ever hand the room back its own next period (
|
|
1321
|
+
* wake armed for it can only ever hand the room back its own next period (see
|
|
1322
1322
|
* `Loop.departureTimers` for the whole argument). The warning below is what a room holding one
|
|
1323
1323
|
* gets instead.
|
|
1324
1324
|
*
|
|
@@ -1372,7 +1372,7 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
1372
1372
|
markDisconnected(clientId: string): void;
|
|
1373
1373
|
ctxFor(clientId: string, reconnecting?: boolean): Ctx;
|
|
1374
1374
|
/**
|
|
1375
|
-
*
|
|
1375
|
+
* The host answering a `room.save()` / `room.kv.*`. The continuation runs here — its
|
|
1376
1376
|
* own event, between ticks, off the back of a host turn — and the flush afterwards is what
|
|
1377
1377
|
* makes "state mutated in a continuation is tracked normally" true rather than aspirational.
|
|
1378
1378
|
*/
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { R as RoomCore } from '../room-
|
|
2
|
-
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-
|
|
1
|
+
import { R as RoomCore } from '../room-BVZiPb6s.js';
|
|
2
|
+
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-BVZiPb6s.js';
|
|
3
3
|
import { ErrorCodeName, FrameType } from '@irtio/protocol';
|
|
4
4
|
import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
5
|
-
import { p as RoomHost, L as LogLevel, k as RoomCoreApi, c as HostCall, d as HostCallResult, W as WriteCaps, r as RoomStats } from '../contract-
|
|
5
|
+
import { p as RoomHost, L as LogLevel, k as RoomCoreApi, c as HostCall, d as HostCallResult, W as WriteCaps, r as RoomStats } from '../contract-BQDN3oje.js';
|
|
6
6
|
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -40,7 +40,7 @@ declare class FakeClock {
|
|
|
40
40
|
* recorded so a test can assert on it: logs, kicks, `close()`, `sleep()`, `crashed()`, and how
|
|
41
41
|
* many frames/bytes went out (per client and in total).
|
|
42
42
|
*
|
|
43
|
-
*
|
|
43
|
+
* It also models the two host capabilities a room can now reach for, both modelled the way the
|
|
44
44
|
* real supervisor models them:
|
|
45
45
|
*
|
|
46
46
|
* - **Host calls** (`room.save()`, `room.kv.*`) are answered on a fake-clock timer, never
|
|
@@ -347,7 +347,7 @@ interface HarnessOptions {
|
|
|
347
347
|
* default, exactly as a deployed room is.
|
|
348
348
|
*/
|
|
349
349
|
readonly traceRpc?: boolean;
|
|
350
|
-
/**
|
|
350
|
+
/** The write-core caps this room runs under. Default: the record cap only. */
|
|
351
351
|
readonly caps?: WriteCaps;
|
|
352
352
|
/** D77: the wall clock hold deadlines are measured against. Default `Date.now`. */
|
|
353
353
|
readonly wallNow?: () => number;
|
package/dist/test/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
initRapier2d,
|
|
7
7
|
isPerClient,
|
|
8
8
|
visibleNames
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-53X6AWQZ.js";
|
|
10
10
|
|
|
11
11
|
// src/test/clock.ts
|
|
12
12
|
var FakeClock = class {
|
|
@@ -389,7 +389,7 @@ function fieldPath(c, names) {
|
|
|
389
389
|
return path;
|
|
390
390
|
}
|
|
391
391
|
function isPlainObject(v) {
|
|
392
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
392
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Uint8Array);
|
|
393
393
|
}
|
|
394
394
|
function mergePatch(target, patch) {
|
|
395
395
|
for (const [k, v] of Object.entries(patch)) {
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LogLevel, t as RpcTraceDump, T as TimelineDump, r as RoomStats, c as HostCall, d as HostCallResult, k as RoomCoreApi, p as RoomHost } from '../contract-
|
|
1
|
+
import { L as LogLevel, t as RpcTraceDump, T as TimelineDump, r as RoomStats, c as HostCall, d as HostCallResult, k as RoomCoreApi, p as RoomHost } from '../contract-BQDN3oje.js';
|
|
2
2
|
import { ErrorCodeName, ProfileSnapshot, PlatformNotice } from '@irtio/protocol';
|
|
3
3
|
import { NpcConfig, LeaveReason } from '@irtio/server';
|
|
4
4
|
import '@irtio/schema';
|
|
@@ -45,13 +45,13 @@ type ToWorker = {
|
|
|
45
45
|
clientId: string;
|
|
46
46
|
role?: string;
|
|
47
47
|
name?: string;
|
|
48
|
-
/**
|
|
48
|
+
/** The verified JWT subject — becomes `JoinOptions.playerId` and therefore
|
|
49
49
|
* `ctx.playerId`. Absent for key joins, whose playerId stays the client id. */
|
|
50
50
|
playerId?: string;
|
|
51
51
|
reconnecting?: boolean;
|
|
52
52
|
/** D44: this join is a scripted NPC's loopback session. */
|
|
53
53
|
npc?: boolean;
|
|
54
|
-
/**
|
|
54
|
+
/** The verified subject, absent for a key join. See `JoinOptions.subject` — it is
|
|
55
55
|
* deliberately narrower than `playerId`, which always has a value. */
|
|
56
56
|
subject?: string;
|
|
57
57
|
/** Issue #123: `JoinOptions.auth`, the credential kind. Absent means `'key'`. */
|
|
@@ -75,7 +75,7 @@ type ToWorker = {
|
|
|
75
75
|
* the room keeps running. `false` is the departure: `onSleep` runs, timers are cleared, pending
|
|
76
76
|
* work is rejected, and the worker is about to be terminated.
|
|
77
77
|
*
|
|
78
|
-
* **Required, deliberately
|
|
78
|
+
* **Required, deliberately.** It was optional, defaulting to the departure, and
|
|
79
79
|
* the periodic `snapshotEveryMs` snapshot of a *running* event-mode room simply omitted it — so
|
|
80
80
|
* every such room ran `onSleep` and lost its armed timers every thirty seconds while it kept
|
|
81
81
|
* running. Only the caller knows which of the two it meant, so the protocol makes it say. Host
|
|
@@ -115,7 +115,7 @@ type ToWorker = {
|
|
|
115
115
|
reqId?: number;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
118
|
+
* The supervisor answering a `hostCall` (a save or a KV operation). It arrives
|
|
119
119
|
* as an ordinary worker message, which is precisely what makes the room's continuation "its
|
|
120
120
|
* own event between ticks" rather than something that ran inside the handler that asked.
|
|
121
121
|
*/
|
|
@@ -243,10 +243,22 @@ type FromWorker = {
|
|
|
243
243
|
* clears nothing and therefore loses nothing. A `room.setInterval` never reports here: it
|
|
244
244
|
* is a cadence rather than a deadline, and a wake armed for one only ever hands the room
|
|
245
245
|
* its own next period, which is how an interval came to mean "this room never stays
|
|
246
|
-
* asleep"
|
|
246
|
+
* asleep". The supervisor arms an internal wake for what does report, so the
|
|
247
247
|
* room is up again by the time its timer would have fired.
|
|
248
248
|
*/
|
|
249
249
|
nextTimerAt?: number;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* The `serialize` reply when there is nothing to send: the room has no core yet, or taking the
|
|
253
|
+
* bytes threw (an encoder error on a bad state value). Answered rather than swallowed so the
|
|
254
|
+
* supervisor learns it at once instead of timing out, and can tell it apart from success: a
|
|
255
|
+
* room whose state could not be captured must be left running, never terminated.
|
|
256
|
+
*/
|
|
257
|
+
| {
|
|
258
|
+
t: 'serialized';
|
|
259
|
+
reqId: number;
|
|
260
|
+
bytes?: undefined;
|
|
261
|
+
error: string;
|
|
250
262
|
} | {
|
|
251
263
|
t: 'inspected';
|
|
252
264
|
reqId: number;
|
|
@@ -309,7 +321,7 @@ type FromWorker = {
|
|
|
309
321
|
* V8 could not be sampled, which the supervisor must report as unread rather than as
|
|
310
322
|
* zero, for the same reason `ticks`/`overruns` are optional.
|
|
311
323
|
*
|
|
312
|
-
*
|
|
324
|
+
* `externalBytes` is the off-heap half — `ArrayBuffer` backing stores, WASM
|
|
313
325
|
* memory, `Buffer`s — which neither `usedBytes` nor the old-generation cap above it covers,
|
|
314
326
|
* and which is therefore the half a room can overshoot its whole budget with while every
|
|
315
327
|
* number the platform was reading stays small. Optional on the wire because an older worker
|
|
@@ -336,7 +348,7 @@ type FromWorker = {
|
|
|
336
348
|
*/
|
|
337
349
|
profile?: ProfileSnapshot;
|
|
338
350
|
}
|
|
339
|
-
/**
|
|
351
|
+
/** The room asked for host-side async work; the supervisor answers `hostResult`. */
|
|
340
352
|
| {
|
|
341
353
|
t: 'hostCall';
|
|
342
354
|
reqId: number;
|
|
@@ -360,6 +372,15 @@ type FromWorker = {
|
|
|
360
372
|
| {
|
|
361
373
|
t: 'setBackfill';
|
|
362
374
|
open: boolean;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* #217: `room.setRole` committed a new role. The supervisor mirrors it onto the session so a
|
|
378
|
+
* later rejoin re-asserts the room's current grant rather than the join-time one.
|
|
379
|
+
*/
|
|
380
|
+
| {
|
|
381
|
+
t: 'role';
|
|
382
|
+
clientId: string;
|
|
383
|
+
role: string;
|
|
363
384
|
} | {
|
|
364
385
|
t: 'setLobby';
|
|
365
386
|
public?: boolean;
|
package/dist/worker/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-GDXHCAD6.js";
|
|
4
4
|
import {
|
|
5
5
|
AdmissionError,
|
|
6
6
|
RoomCore,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
initRapier2d,
|
|
11
11
|
onFirstRapierStep,
|
|
12
12
|
rapierHasStepped
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-53X6AWQZ.js";
|
|
14
14
|
|
|
15
15
|
// src/worker/index.ts
|
|
16
16
|
import { getHeapStatistics } from "v8";
|
|
@@ -81,6 +81,9 @@ function createWorkerHost(post) {
|
|
|
81
81
|
setBackfill(open) {
|
|
82
82
|
post({ t: "setBackfill", open });
|
|
83
83
|
},
|
|
84
|
+
roleChanged(clientId, role) {
|
|
85
|
+
post({ t: "role", clientId, role });
|
|
86
|
+
},
|
|
84
87
|
// ---- M6 lane H: lobby front door ----
|
|
85
88
|
setLobby(update) {
|
|
86
89
|
post({ t: "setLobby", ...update });
|
|
@@ -322,8 +325,19 @@ async function handleMessage(state, host, post, msg) {
|
|
|
322
325
|
}
|
|
323
326
|
case "serialize": {
|
|
324
327
|
const core = state.core;
|
|
325
|
-
if (!core)
|
|
326
|
-
|
|
328
|
+
if (!core) {
|
|
329
|
+
post({ t: "serialized", reqId: msg.reqId, error: "room is not initialized" });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let bytes;
|
|
333
|
+
try {
|
|
334
|
+
bytes = ownBuffer(msg.forSave ? core.snapshot() : core.serialize());
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const detail = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
337
|
+
post({ t: "log", level: "error", args: ["worker: handling 'serialize' threw", detail] });
|
|
338
|
+
post({ t: "serialized", reqId: msg.reqId, error: detail });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
327
341
|
const timerAt = msg.forSave ? void 0 : core.lastSerializeTimerAt;
|
|
328
342
|
post(
|
|
329
343
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
15
18
|
"sideEffects": false,
|
|
16
19
|
"main": "./dist/index.js",
|
|
17
20
|
"types": "./dist/index.d.ts",
|
|
@@ -36,9 +39,9 @@
|
|
|
36
39
|
"@dimforge/rapier2d-compat": "0.20.0",
|
|
37
40
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
38
41
|
"matter-js": "0.20.0",
|
|
39
|
-
"@irtio/protocol": "^4.
|
|
40
|
-
"@irtio/
|
|
41
|
-
"@irtio/
|
|
42
|
+
"@irtio/protocol": "^4.8.0",
|
|
43
|
+
"@irtio/schema": "^4.8.0",
|
|
44
|
+
"@irtio/server": "^4.8.0"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@types/matter-js": "0.20.2"
|