@irtio/runtime 4.0.0 → 4.2.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/README.md +56 -0
- package/dist/{chunk-S4D472RL.js → chunk-KJN62J37.js} +75 -27
- package/dist/{chunk-G2XKCXRO.js → chunk-ZGJY4E3G.js} +1 -1
- package/dist/{contract-CpBkqkn7.d.ts → contract-DHWlURaq.d.ts} +7 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.js +4 -2
- package/dist/{room-B9h9dc4e.d.ts → room-Bpo_nqlO.d.ts} +7 -2
- package/dist/test/index.d.ts +3 -3
- package/dist/test/index.js +14 -6
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +12 -2
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @irtio/runtime
|
|
2
|
+
|
|
3
|
+
Execute irtio room definitions in a custom host or an in-process harness. The runtime applies
|
|
4
|
+
client input, runs handlers, encodes state updates, and saves and restores rooms.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @irtio/runtime @irtio/server @irtio/schema
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
For game development, use [`irtio dev`](https://irt.io/docs/reference/cli#irtio-dev) to run a room
|
|
11
|
+
and [`@irtio/testing`](https://irt.io/docs/reference/testing) to test it with real SDK clients.
|
|
12
|
+
|
|
13
|
+
## Run a room with the low-level harness
|
|
14
|
+
|
|
15
|
+
This TypeScript example creates a room and advances one server tick:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineSchema, singleton, u32 } from '@irtio/schema';
|
|
19
|
+
import { defineRoom } from '@irtio/server';
|
|
20
|
+
import { createRoomHarness } from '@irtio/runtime/test';
|
|
21
|
+
|
|
22
|
+
const schema = defineSchema({ counter: singleton({ ticks: u32 }) });
|
|
23
|
+
const definition = defineRoom(schema, {
|
|
24
|
+
mode: 'tick',
|
|
25
|
+
tickRate: 20,
|
|
26
|
+
tick(state) {
|
|
27
|
+
state.counter.ticks += 1;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const harness = createRoomHarness(definition);
|
|
32
|
+
try {
|
|
33
|
+
harness.tick();
|
|
34
|
+
console.log(harness.state.counter.ticks); // 1
|
|
35
|
+
} finally {
|
|
36
|
+
harness.stop();
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The low-level harness uses a fake clock and protocol clients. Its `tick()` and `run(ms)` methods
|
|
41
|
+
are synchronous. This differs from the async methods in `@irtio/testing`.
|
|
42
|
+
|
|
43
|
+
## Entry points
|
|
44
|
+
|
|
45
|
+
| Import | Use |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `@irtio/runtime` | `RoomCore`, host contracts, snapshots, migrations, and inspection |
|
|
48
|
+
| `@irtio/runtime/test` | `createRoomHarness`, `FakeClock`, and test-host helpers |
|
|
49
|
+
| `@irtio/runtime/worker` | Node worker host and typed worker messages |
|
|
50
|
+
|
|
51
|
+
A custom host supplies the `RoomHost` contract, including time, scheduling, frame delivery, and
|
|
52
|
+
room lifecycle operations. Keep credentials and transport admission checks at your host boundary.
|
|
53
|
+
Initialize the appropriate physics engine before constructing a room that uses it.
|
|
54
|
+
|
|
55
|
+
See the [server room API](https://irt.io/docs/reference/room) for the behavior room handlers expect
|
|
56
|
+
and [hibernation](https://irt.io/docs/concepts/hibernation) for persistence and restoration rules.
|
|
@@ -397,6 +397,22 @@ function isVisible(c, role) {
|
|
|
397
397
|
if (c.visibility === "server") return false;
|
|
398
398
|
return c.visibility !== "role" || (c.roles ?? []).includes(role);
|
|
399
399
|
}
|
|
400
|
+
function isPerClient(c) {
|
|
401
|
+
return c.visibility === "spatial-grid" || c.visibility === "owner";
|
|
402
|
+
}
|
|
403
|
+
var perClientCache = /* @__PURE__ */ new WeakMap();
|
|
404
|
+
function perClientDescs(ext) {
|
|
405
|
+
let hit = perClientCache.get(ext);
|
|
406
|
+
if (!hit) {
|
|
407
|
+
const all = ext.collections;
|
|
408
|
+
hit = {
|
|
409
|
+
spatial: all.filter((c) => c.visibility === "spatial-grid"),
|
|
410
|
+
perClient: all.filter(isPerClient)
|
|
411
|
+
};
|
|
412
|
+
perClientCache.set(ext, hit);
|
|
413
|
+
}
|
|
414
|
+
return hit;
|
|
415
|
+
}
|
|
400
416
|
function visibleTo(role) {
|
|
401
417
|
return (c) => isVisible(c, role);
|
|
402
418
|
}
|
|
@@ -421,20 +437,30 @@ function encodeViewSnapshot(ext, plain, role, tick, memberships) {
|
|
|
421
437
|
{ tick },
|
|
422
438
|
{
|
|
423
439
|
collections: visibleTo(role),
|
|
424
|
-
entities: (c, id) => c
|
|
440
|
+
entities: (c, id) => !isPerClient(c) || (memberships?.get(c.name)?.has(id) ?? false)
|
|
425
441
|
}
|
|
426
442
|
);
|
|
427
443
|
}
|
|
428
|
-
function
|
|
444
|
+
function ownedIds(plain, desc, clientId) {
|
|
445
|
+
const out = /* @__PURE__ */ new Set();
|
|
446
|
+
if (clientId === "") return out;
|
|
447
|
+
const coll = plainEntity(plain, desc.name);
|
|
448
|
+
for (const id of coll.ids()) if (coll.ownerOf(id) === clientId) out.add(id);
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
function viewMemberships(ext, plain, clientId, role, indexes = buildSpatialIndexes(ext.collections, plain)) {
|
|
429
452
|
const out = /* @__PURE__ */ new Map();
|
|
430
453
|
for (const [name, index] of indexes) out.set(name, querySpatialIndex(index, clientId, role));
|
|
454
|
+
for (const c of ext.collections) {
|
|
455
|
+
if (c.visibility === "owner") out.set(c.name, ownedIds(plain, c, clientId));
|
|
456
|
+
}
|
|
431
457
|
return out;
|
|
432
458
|
}
|
|
433
459
|
var aoiMemo = /* @__PURE__ */ new WeakMap();
|
|
434
460
|
function inClientView(core, clientId, desc, id) {
|
|
435
461
|
const role = core.clients.get(clientId)?.role ?? "";
|
|
436
462
|
if (!isVisible(desc, role)) return false;
|
|
437
|
-
if (desc
|
|
463
|
+
if (!isPerClient(desc)) return true;
|
|
438
464
|
let memo = aoiMemo.get(core);
|
|
439
465
|
if (!memo || memo.tick !== core.tick) {
|
|
440
466
|
memo = { tick: core.tick, byClient: /* @__PURE__ */ new Map() };
|
|
@@ -443,18 +469,23 @@ function inClientView(core, clientId, desc, id) {
|
|
|
443
469
|
const key = `${clientId}\0${role}`;
|
|
444
470
|
let mine = memo.byClient.get(key);
|
|
445
471
|
if (!mine) {
|
|
446
|
-
mine =
|
|
472
|
+
mine = viewMemberships(core.ext, core.plain, clientId, role);
|
|
447
473
|
memo.byClient.set(key, mine);
|
|
448
474
|
}
|
|
449
475
|
return mine.get(desc.name)?.has(id) ?? false;
|
|
450
476
|
}
|
|
451
477
|
function createVisibilityPolicy(ext, plain, clientId, role) {
|
|
452
|
-
const memberships =
|
|
478
|
+
const memberships = viewMemberships(ext, plain, clientId, role);
|
|
453
479
|
return {
|
|
454
480
|
memberships,
|
|
455
481
|
maySeeCollection: (desc) => isVisible(desc, role),
|
|
456
|
-
maySeeEntity: (desc, id) => isVisible(desc, role) && (desc
|
|
482
|
+
maySeeEntity: (desc, id) => isVisible(desc, role) && (!isPerClient(desc) || (memberships.get(desc.name)?.has(id) ?? false)),
|
|
457
483
|
describeSpatial: (desc, ids) => {
|
|
484
|
+
if (desc.visibility === "owner") {
|
|
485
|
+
const coll2 = plainEntity(plain, desc.name);
|
|
486
|
+
const at2 = (id) => `${id} owned by ${coll2.ownerOf(id) || "<server>"}`;
|
|
487
|
+
return `viewer ${clientId}, ${desc.name} ${ids.map(at2).join(" ")}`;
|
|
488
|
+
}
|
|
458
489
|
const grid = desc.grid;
|
|
459
490
|
if (desc.visibility !== "spatial-grid" || !grid) return void 0;
|
|
460
491
|
const coll = plainEntity(plain, desc.name);
|
|
@@ -469,13 +500,13 @@ function createVisibilityPolicy(ext, plain, clientId, role) {
|
|
|
469
500
|
function sharedViewDirty(ext, dirty, role) {
|
|
470
501
|
return filterDirty(dirty, (name) => {
|
|
471
502
|
const desc = ext.collection(name);
|
|
472
|
-
return desc
|
|
503
|
+
return !isPerClient(desc) && isVisible(desc, role);
|
|
473
504
|
});
|
|
474
505
|
}
|
|
475
|
-
function
|
|
506
|
+
function perClientViewDirty(ext, dirty, previous, current) {
|
|
476
507
|
const out = createDirtySet();
|
|
477
508
|
for (const desc of ext.collections) {
|
|
478
|
-
if (desc
|
|
509
|
+
if (!isPerClient(desc)) continue;
|
|
479
510
|
const before = previous.get(desc.name) ?? /* @__PURE__ */ new Set();
|
|
480
511
|
const now = current.get(desc.name) ?? /* @__PURE__ */ new Set();
|
|
481
512
|
for (const id of before) if (!now.has(id)) markRemove(out, desc.name, id);
|
|
@@ -497,7 +528,7 @@ function membershipSafeDirty(ext, dirty, memberships) {
|
|
|
497
528
|
const out = createDirtySet();
|
|
498
529
|
for (const [name, changed] of dirty) {
|
|
499
530
|
const desc = ext.collection(name);
|
|
500
|
-
if (desc
|
|
531
|
+
if (!isPerClient(desc)) {
|
|
501
532
|
out.set(name, changed);
|
|
502
533
|
continue;
|
|
503
534
|
}
|
|
@@ -563,13 +594,16 @@ function catchUpDirty(ext, plain, fromRole, toRole) {
|
|
|
563
594
|
import {
|
|
564
595
|
ErrorCode as ErrorCode2,
|
|
565
596
|
FrameType as FrameType2,
|
|
597
|
+
NO_REPLY_REQ_ID_BIT,
|
|
566
598
|
OWNERSHIP_ASK,
|
|
599
|
+
REQ_ID_MASK,
|
|
567
600
|
clampErrorMessage,
|
|
568
601
|
decodeCall,
|
|
569
602
|
decodeReply,
|
|
570
603
|
encodeCall,
|
|
571
604
|
encodeFrame as encodeFrame2,
|
|
572
605
|
encodeReply,
|
|
606
|
+
isNoReplyReqId,
|
|
573
607
|
isReservedRpcName,
|
|
574
608
|
rpcByIdOf,
|
|
575
609
|
rpcTable
|
|
@@ -1152,10 +1186,10 @@ function descFor(core, name) {
|
|
|
1152
1186
|
}
|
|
1153
1187
|
return found;
|
|
1154
1188
|
}
|
|
1155
|
-
function sendCall(core, clientId, desc, params) {
|
|
1189
|
+
function sendCall(core, clientId, desc, params, noReply = false) {
|
|
1156
1190
|
const s = stateOf3(core);
|
|
1157
|
-
const reqId = s.nextReqId;
|
|
1158
|
-
s.nextReqId = s.nextReqId + 1
|
|
1191
|
+
const reqId = noReply ? (s.nextReqId | NO_REPLY_REQ_ID_BIT) >>> 0 : s.nextReqId;
|
|
1192
|
+
s.nextReqId = s.nextReqId + 1 & REQ_ID_MASK || 1;
|
|
1159
1193
|
const bytes = encodeFields(desc.params, params);
|
|
1160
1194
|
core.send(
|
|
1161
1195
|
clientId,
|
|
@@ -1193,13 +1227,20 @@ function createCallProxy(core, clientId) {
|
|
|
1193
1227
|
if (typeof prop !== "string") return void 0;
|
|
1194
1228
|
return (params) => {
|
|
1195
1229
|
const desc = descFor(core, prop);
|
|
1196
|
-
|
|
1230
|
+
const p = new Promise((resolve, reject2) => {
|
|
1197
1231
|
callClient(core, clientId, desc, params ?? {}, {
|
|
1198
1232
|
timeoutMs: RPC_TIMEOUT_MS,
|
|
1199
1233
|
resolve,
|
|
1200
1234
|
reject: reject2
|
|
1201
1235
|
});
|
|
1202
1236
|
});
|
|
1237
|
+
void p.catch((err) => {
|
|
1238
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1239
|
+
core.stats.rpcClientFailures++;
|
|
1240
|
+
core.log("warn", `rpc ${desc.name} to ${clientId} failed: ${message}`);
|
|
1241
|
+
core.recordEvent("error", clientId, `rpc ${desc.name}: ${message}`);
|
|
1242
|
+
});
|
|
1243
|
+
return p;
|
|
1203
1244
|
};
|
|
1204
1245
|
}
|
|
1205
1246
|
}
|
|
@@ -1216,7 +1257,7 @@ function createBroadcastProxy(core, except) {
|
|
|
1216
1257
|
for (const entry of core.clients.values()) {
|
|
1217
1258
|
if (!entry.connected) continue;
|
|
1218
1259
|
if (except?.has(entry.clientId)) continue;
|
|
1219
|
-
sendCall(core, entry.clientId, desc, params ?? {});
|
|
1260
|
+
sendCall(core, entry.clientId, desc, params ?? {}, true);
|
|
1220
1261
|
}
|
|
1221
1262
|
};
|
|
1222
1263
|
}
|
|
@@ -1239,6 +1280,7 @@ function handleReply(core, clientId, payload) {
|
|
|
1239
1280
|
const key = pendingKey(clientId, reply.reqId);
|
|
1240
1281
|
const pending = s.pending.get(key);
|
|
1241
1282
|
if (!pending) {
|
|
1283
|
+
if (isNoReplyReqId(reply.reqId)) return true;
|
|
1242
1284
|
core.log("warn", `REPLY from ${clientId} for unknown reqId ${reply.reqId}`);
|
|
1243
1285
|
return true;
|
|
1244
1286
|
}
|
|
@@ -1503,7 +1545,7 @@ function applyOp(core, clientId, c, op) {
|
|
|
1503
1545
|
);
|
|
1504
1546
|
return;
|
|
1505
1547
|
}
|
|
1506
|
-
if (c.kind === "entity" && c
|
|
1548
|
+
if (c.kind === "entity" && isPerClient(c)) {
|
|
1507
1549
|
const coll = plainEntity(core.plain, c.name);
|
|
1508
1550
|
if (coll.has(op.id) && coll.ownerOf(op.id) !== clientId && !inClientView(core, clientId, c, op.id)) {
|
|
1509
1551
|
core.recordEvent("write-rejected", clientId, `${c.name}: ${op.id} is not in view`);
|
|
@@ -5137,7 +5179,7 @@ function stateOf6(core) {
|
|
|
5137
5179
|
function omniscientMemberships(core) {
|
|
5138
5180
|
const out = /* @__PURE__ */ new Map();
|
|
5139
5181
|
for (const desc of core.ext.collections) {
|
|
5140
|
-
if (desc
|
|
5182
|
+
if (!isPerClient(desc)) continue;
|
|
5141
5183
|
out.set(desc.name, new Set(plainEntity(core.plain, desc.name).ids()));
|
|
5142
5184
|
}
|
|
5143
5185
|
return out;
|
|
@@ -5146,9 +5188,7 @@ function hasRoleScoped(core) {
|
|
|
5146
5188
|
return core.ext.collections.some((c) => c.visibility === "role");
|
|
5147
5189
|
}
|
|
5148
5190
|
function hasSpatial(core) {
|
|
5149
|
-
return core.ext.collections.some(
|
|
5150
|
-
(c) => c.visibility === "spatial-grid"
|
|
5151
|
-
);
|
|
5191
|
+
return core.ext.collections.some(isPerClient);
|
|
5152
5192
|
}
|
|
5153
5193
|
function recorderSnapshot(core, state) {
|
|
5154
5194
|
if (state.view === "") {
|
|
@@ -6192,6 +6232,7 @@ var RoomCore = class _RoomCore {
|
|
|
6192
6232
|
bytesOut: 0,
|
|
6193
6233
|
bytesOutByClient: /* @__PURE__ */ new Map(),
|
|
6194
6234
|
handlerErrors: 0,
|
|
6235
|
+
rpcClientFailures: 0,
|
|
6195
6236
|
encodesLastFlush: 0,
|
|
6196
6237
|
aoiEncodesLastFlush: 0,
|
|
6197
6238
|
gridBuildMs: 0,
|
|
@@ -6793,6 +6834,9 @@ var RoomCore = class _RoomCore {
|
|
|
6793
6834
|
role: this.resolveRole(options.role, void 0),
|
|
6794
6835
|
name: options.name ?? "",
|
|
6795
6836
|
tick: this.tick,
|
|
6837
|
+
// Bug #133: no `ClientEntry` exists yet, so the flag comes straight from the join options —
|
|
6838
|
+
// the same value line 1130 will stamp on the entry a moment later if this admission passes.
|
|
6839
|
+
npc: options.npc === true,
|
|
6796
6840
|
deny: (reason) => {
|
|
6797
6841
|
throw new DenyError2(reason);
|
|
6798
6842
|
},
|
|
@@ -6854,7 +6898,7 @@ var RoomCore = class _RoomCore {
|
|
|
6854
6898
|
this.plain,
|
|
6855
6899
|
role2,
|
|
6856
6900
|
this.tick,
|
|
6857
|
-
|
|
6901
|
+
viewMemberships(this.ext, this.plain, clientId, role2)
|
|
6858
6902
|
)
|
|
6859
6903
|
};
|
|
6860
6904
|
}
|
|
@@ -6929,7 +6973,7 @@ var RoomCore = class _RoomCore {
|
|
|
6929
6973
|
}
|
|
6930
6974
|
if (!entry.npc) this.loop.noteActivity();
|
|
6931
6975
|
this.recordEvent("join", clientId, reconnecting ? "reconnect" : void 0);
|
|
6932
|
-
const memberships =
|
|
6976
|
+
const memberships = viewMemberships(this.ext, this.plain, clientId, entry.role);
|
|
6933
6977
|
entry.spatialMembership = memberships;
|
|
6934
6978
|
const snapshot = encodeViewSnapshot(this.ext, this.plain, entry.role, this.tick, memberships);
|
|
6935
6979
|
entry.welcomed = true;
|
|
@@ -7035,6 +7079,9 @@ var RoomCore = class _RoomCore {
|
|
|
7035
7079
|
// other entry point (join, leave, write, ownership) has no client tick by construction.
|
|
7036
7080
|
clientTick: void 0,
|
|
7037
7081
|
reconnecting,
|
|
7082
|
+
// Bug #133: read from the seat, not from anything the session says about itself — only
|
|
7083
|
+
// `room.spawnNPC`'s loopback join sets it, so an RPC ctx reports it honestly too.
|
|
7084
|
+
npc: entry?.npc ?? false,
|
|
7038
7085
|
room: this.room,
|
|
7039
7086
|
// M7 pf4 (a): on every ctx rather than only the RPC one, because the type says it is there.
|
|
7040
7087
|
// `rpc.ts` is what turns the throw into an `ok: false` reply; anywhere else it is a throw.
|
|
@@ -7233,7 +7280,7 @@ var RoomCore = class _RoomCore {
|
|
|
7233
7280
|
this.ledger?.newFlush();
|
|
7234
7281
|
this.stats.encodesLastFlush = 0;
|
|
7235
7282
|
this.stats.aoiEncodesLastFlush = 0;
|
|
7236
|
-
const spatialDescs = this.ext
|
|
7283
|
+
const { spatial: spatialDescs, perClient } = perClientDescs(this.ext);
|
|
7237
7284
|
const buildStarted = performance.now();
|
|
7238
7285
|
const indexes = buildSpatialIndexes(spatialDescs, this.plain);
|
|
7239
7286
|
this.stats.gridBuildMs += performance.now() - buildStarted;
|
|
@@ -7241,7 +7288,7 @@ var RoomCore = class _RoomCore {
|
|
|
7241
7288
|
for (const entry of this.clients.values()) {
|
|
7242
7289
|
if (entry.connected) {
|
|
7243
7290
|
const queryStarted = performance.now();
|
|
7244
|
-
const memberships =
|
|
7291
|
+
const memberships = viewMemberships(
|
|
7245
7292
|
this.ext,
|
|
7246
7293
|
this.plain,
|
|
7247
7294
|
entry.clientId,
|
|
@@ -7284,7 +7331,7 @@ var RoomCore = class _RoomCore {
|
|
|
7284
7331
|
}
|
|
7285
7332
|
}
|
|
7286
7333
|
let delta;
|
|
7287
|
-
if (
|
|
7334
|
+
if (perClient.length > 0) {
|
|
7288
7335
|
const sourceDirty = entry.pendingJoinAdds ? stripAdds(this.plain, dirty, entry.pendingJoinAdds) : dirty;
|
|
7289
7336
|
let shared;
|
|
7290
7337
|
if (entry.pendingJoinAdds) {
|
|
@@ -7309,7 +7356,7 @@ var RoomCore = class _RoomCore {
|
|
|
7309
7356
|
this.ledger ? { shared } : void 0
|
|
7310
7357
|
);
|
|
7311
7358
|
}
|
|
7312
|
-
const aoiDirty =
|
|
7359
|
+
const aoiDirty = perClientViewDirty(
|
|
7313
7360
|
this.ext,
|
|
7314
7361
|
sourceDirty,
|
|
7315
7362
|
entry.spatialMembership,
|
|
@@ -7350,7 +7397,7 @@ var RoomCore = class _RoomCore {
|
|
|
7350
7397
|
encodeFrame5(FrameType6.DELTA, delta),
|
|
7351
7398
|
this.ledger ? {
|
|
7352
7399
|
...churn ? { churn } : {},
|
|
7353
|
-
...
|
|
7400
|
+
...perClient.length > 0 ? {} : { shared: delta }
|
|
7354
7401
|
} : void 0
|
|
7355
7402
|
);
|
|
7356
7403
|
}
|
|
@@ -7389,6 +7436,7 @@ export {
|
|
|
7389
7436
|
summarize,
|
|
7390
7437
|
RpcTraceRing,
|
|
7391
7438
|
isVisible,
|
|
7439
|
+
isPerClient,
|
|
7392
7440
|
visibleTo,
|
|
7393
7441
|
visibleNames,
|
|
7394
7442
|
viewKeyFor,
|
|
@@ -604,6 +604,13 @@ interface RoomStats {
|
|
|
604
604
|
/** Per connected client. */
|
|
605
605
|
bytesOutByClient: Map<string, number>;
|
|
606
606
|
handlerErrors: number;
|
|
607
|
+
/**
|
|
608
|
+
* Bug #131: server→client RPCs (`room.call(id).someRpc(...)`) that rejected — the client has no
|
|
609
|
+
* implementation, disconnected, or did not answer within the 5 s window. Counted whether or not
|
|
610
|
+
* the room's own code attached a `.catch`, and deliberately *not* a `handlerErrors`: nothing the
|
|
611
|
+
* room's code did threw.
|
|
612
|
+
*/
|
|
613
|
+
rpcClientFailures: number;
|
|
607
614
|
encodesLastFlush: number;
|
|
608
615
|
aoiEncodesLastFlush: number;
|
|
609
616
|
gridBuildMs: number;
|
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-DHWlURaq.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-Bpo_nqlO.js';
|
|
3
3
|
import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
|
|
4
4
|
import '@irtio/protocol';
|
|
5
5
|
import '@irtio/server';
|
|
@@ -26,6 +26,12 @@ declare const RPC_TIMEOUT_MS = 5000;
|
|
|
26
26
|
*
|
|
27
27
|
* A collection with `visibility: 'role'` is visible only to the roles it names — including its
|
|
28
28
|
* adds and removes, so a client never learns such a record exists.
|
|
29
|
+
*
|
|
30
|
+
* Two modes filter *entities* rather than collections, and both are handled by the same
|
|
31
|
+
* per-client membership machinery: `visibility: 'spatial-grid'` (the ids inside the client's AOI)
|
|
32
|
+
* and `visibility: 'owner'` (the ids the client owns, per the first-class `ownerOf` ownership
|
|
33
|
+
* every entity record carries). A server-owned record — owner `SERVER_OWNER`, the empty string —
|
|
34
|
+
* is owned by no client and is therefore visible to none.
|
|
29
35
|
*/
|
|
30
36
|
|
|
31
37
|
/**
|
|
@@ -33,6 +39,13 @@ declare const RPC_TIMEOUT_MS = 5000;
|
|
|
33
39
|
* answer `visibility: 'role'` with an empty `roles` list has always given, said on purpose.
|
|
34
40
|
*/
|
|
35
41
|
declare function isVisible(c: CollectionDesc, role: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Is `c` filtered per client rather than per role? `spatial-grid` by AOI membership, `owner` by
|
|
44
|
+
* record ownership. Both answer `isVisible` with `true` — from the role's side the collection
|
|
45
|
+
* genuinely is visible, and it is the entities that come and go — so every path that encodes or
|
|
46
|
+
* judges entities has to ask this question as well.
|
|
47
|
+
*/
|
|
48
|
+
declare function isPerClient(c: CollectionDesc): boolean;
|
|
36
49
|
/** A `SnapshotOptions.collections` predicate for one role. */
|
|
37
50
|
declare function visibleTo(role: string): (c: CollectionDesc) => boolean;
|
|
38
51
|
/** Names of the collections `role` sees (memoised per schema + role). */
|
|
@@ -48,7 +61,10 @@ interface VisibilityPolicy {
|
|
|
48
61
|
readonly memberships: ReadonlyMap<string, ReadonlySet<string>>;
|
|
49
62
|
maySeeCollection(desc: CollectionDesc): boolean;
|
|
50
63
|
maySeeEntity(desc: CollectionDesc, id: string): boolean;
|
|
51
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Viewer cell, offending cells, and the radius — the diagnostics a leak report needs. For an
|
|
66
|
+
* `owner` collection it names the actual owner of each offending id instead.
|
|
67
|
+
*/
|
|
52
68
|
describeSpatial(desc: CollectionDesc, ids: readonly string[]): string | undefined;
|
|
53
69
|
}
|
|
54
70
|
/** Reusable authority-based policy for runtime, harness, and bot leak checks. */
|
|
@@ -245,4 +261,4 @@ declare function migrateSnapshot(blob: Uint8Array, chain: readonly MigrationStep
|
|
|
245
261
|
declare function toMigrationState(schema: AnySchema, plain: PlainState): MigrationState;
|
|
246
262
|
declare function fromMigrationState(schema: AnySchema, state: MigrationState, version: number): PlainState;
|
|
247
263
|
|
|
248
|
-
export { type DecodedSave, type MigrateOptions, type MigrationEntity, type MigrationHelpers, type MigrationModule, type MigrationOutcome, type MigrationRecord, type MigrationState, type MigrationStep, type ParsedSnapshot, READABLE_SNAPSHOT_VERSIONS, RPC_TIMEOUT_MS, SNAPSHOT_FORMAT_VERSION, type SnapshotHeader, type SnapshotMode, type VisibilityPolicy, catchUpDirty, createVisibilityPolicy, decodeSave, encodeViewDelta, encodeViewSnapshot, fromMigrationState, isVisible, migrateSnapshot, parseHibernationBlob, toMigrationState, viewKeyFor, visibleNames, visibleTo, writeHibernationBlob };
|
|
264
|
+
export { type DecodedSave, type MigrateOptions, type MigrationEntity, type MigrationHelpers, type MigrationModule, type MigrationOutcome, type MigrationRecord, type MigrationState, type MigrationStep, type ParsedSnapshot, READABLE_SNAPSHOT_VERSIONS, RPC_TIMEOUT_MS, SNAPSHOT_FORMAT_VERSION, type SnapshotHeader, type SnapshotMode, type VisibilityPolicy, catchUpDirty, createVisibilityPolicy, decodeSave, encodeViewDelta, encodeViewSnapshot, fromMigrationState, isPerClient, isVisible, migrateSnapshot, parseHibernationBlob, toMigrationState, viewKeyFor, visibleNames, visibleTo, writeHibernationBlob };
|
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-ZGJY4E3G.js";
|
|
6
6
|
import {
|
|
7
7
|
AdmissionError,
|
|
8
8
|
CRASH_AFTER_THROWS,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
initPhysics,
|
|
45
45
|
initRapier2d,
|
|
46
46
|
inspectState,
|
|
47
|
+
isPerClient,
|
|
47
48
|
isVisible,
|
|
48
49
|
loadedMatter,
|
|
49
50
|
loadedPhysics,
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
visibleNames,
|
|
59
60
|
visibleTo,
|
|
60
61
|
writeHibernationBlob
|
|
61
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-KJN62J37.js";
|
|
62
63
|
export {
|
|
63
64
|
AdmissionError,
|
|
64
65
|
CRASH_AFTER_THROWS,
|
|
@@ -101,6 +102,7 @@ export {
|
|
|
101
102
|
initPhysics,
|
|
102
103
|
initRapier2d,
|
|
103
104
|
inspectState,
|
|
105
|
+
isPerClient,
|
|
104
106
|
isVisible,
|
|
105
107
|
loadedMatter,
|
|
106
108
|
loadedPhysics,
|
|
@@ -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, 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-DHWlURaq.js';
|
|
5
5
|
|
|
6
6
|
declare class Mulberry32 {
|
|
7
7
|
/** Current internal state (u32). Survives hibernation. */
|
|
@@ -83,7 +83,12 @@ interface ClientEntry {
|
|
|
83
83
|
* Every other client's delta is unaffected.
|
|
84
84
|
*/
|
|
85
85
|
pendingJoinAdds: Map<string, ReadonlyMap<string, JoinAddCapture>> | undefined;
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Last per-client entity membership sent to this client, keyed by collection: AOI membership
|
|
88
|
+
* for a `spatial-grid` collection, the ids it owns for an `owner` one. The diff against this
|
|
89
|
+
* flush's membership is what synthesises the adds and removes neither the dirty set nor the
|
|
90
|
+
* shared per-role payload can carry.
|
|
91
|
+
*/
|
|
87
92
|
spatialMembership: Map<string, Set<string>>;
|
|
88
93
|
}
|
|
89
94
|
/** What `join` captured for one of a client's own just-added records, for `flush`'s comparison. */
|
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-Bpo_nqlO.js';
|
|
2
|
+
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-Bpo_nqlO.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-DHWlURaq.js';
|
|
6
6
|
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
7
7
|
|
|
8
8
|
/**
|
package/dist/test/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
initMatter,
|
|
5
5
|
initPhysics,
|
|
6
6
|
initRapier2d,
|
|
7
|
+
isPerClient,
|
|
7
8
|
visibleNames
|
|
8
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-KJN62J37.js";
|
|
9
10
|
|
|
10
11
|
// src/test/clock.ts
|
|
11
12
|
var FakeClock = class {
|
|
@@ -352,6 +353,7 @@ import {
|
|
|
352
353
|
encodeFrame,
|
|
353
354
|
encodeMsg,
|
|
354
355
|
encodeReply,
|
|
356
|
+
isNoReplyReqId,
|
|
355
357
|
rpcTable
|
|
356
358
|
} from "@irtio/protocol";
|
|
357
359
|
import {
|
|
@@ -621,7 +623,7 @@ var FakeClientImpl = class {
|
|
|
621
623
|
/** Flags any collection the frame named that this client's role may not see. */
|
|
622
624
|
checkFrameVisibility(delta) {
|
|
623
625
|
for (const dc of delta.collections) {
|
|
624
|
-
const spatial = this.bridge.ext.collection(dc.name)
|
|
626
|
+
const spatial = isPerClient(this.bridge.ext.collection(dc.name));
|
|
625
627
|
const leaked = dc.ops.filter((op) => {
|
|
626
628
|
if (!this.bridge.visible(this.role, this.id, dc.name)) return true;
|
|
627
629
|
if (!spatial) return false;
|
|
@@ -660,20 +662,26 @@ var FakeClientImpl = class {
|
|
|
660
662
|
this.calls.push({ name: desc.name, params });
|
|
661
663
|
const impl = this.impls.get(desc.name);
|
|
662
664
|
if (!impl) return;
|
|
665
|
+
const noReply = isNoReplyReqId(call.reqId);
|
|
663
666
|
let result;
|
|
664
667
|
try {
|
|
665
668
|
result = impl(params);
|
|
666
669
|
} catch (err) {
|
|
667
|
-
this.replyError(call.reqId, err);
|
|
670
|
+
if (!noReply) this.replyError(call.reqId, err);
|
|
668
671
|
return;
|
|
669
672
|
}
|
|
670
673
|
if (isThenable(result)) {
|
|
671
674
|
Promise.resolve(result).then(
|
|
672
|
-
(v) =>
|
|
673
|
-
|
|
675
|
+
(v) => {
|
|
676
|
+
if (!noReply) this.replyOk(desc, call.reqId, v);
|
|
677
|
+
},
|
|
678
|
+
(err) => {
|
|
679
|
+
if (!noReply) this.replyError(call.reqId, err);
|
|
680
|
+
}
|
|
674
681
|
);
|
|
675
682
|
return;
|
|
676
683
|
}
|
|
684
|
+
if (noReply) return;
|
|
677
685
|
this.replyOk(desc, call.reqId, result);
|
|
678
686
|
}
|
|
679
687
|
replyOk(desc, reqId, result) {
|
|
@@ -1010,7 +1018,7 @@ var Harness = class {
|
|
|
1010
1018
|
const keep = visibleNames(ext, client.role);
|
|
1011
1019
|
const policy = createVisibilityPolicy(ext, this.coreRef.plain, client.id, client.role);
|
|
1012
1020
|
for (const c of ext.collections) {
|
|
1013
|
-
if (c
|
|
1021
|
+
if (isPerClient(c)) {
|
|
1014
1022
|
const coll = client.view[c.name];
|
|
1015
1023
|
const ids = coll ? [...coll.ids()].filter((id) => !policy.maySeeEntity(c, id)) : [];
|
|
1016
1024
|
if (ids.length > 0) {
|
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-DHWlURaq.js';
|
|
2
2
|
import { ErrorCodeName, ProfileSnapshot, PlatformNotice } from '@irtio/protocol';
|
|
3
3
|
import { NpcConfig, LeaveReason } from '@irtio/server';
|
|
4
4
|
import '@irtio/schema';
|
package/dist/worker/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-ZGJY4E3G.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-KJN62J37.js";
|
|
14
14
|
|
|
15
15
|
// src/worker/index.ts
|
|
16
16
|
import { getHeapStatistics } from "v8";
|
|
@@ -452,6 +452,16 @@ if (!isMainThread && parentPort) {
|
|
|
452
452
|
const post = (msg, transfer) => {
|
|
453
453
|
port.postMessage(msg, transfer ?? transferList(msg));
|
|
454
454
|
};
|
|
455
|
+
process.on("unhandledRejection", (reason) => {
|
|
456
|
+
post({
|
|
457
|
+
t: "log",
|
|
458
|
+
level: "error",
|
|
459
|
+
args: [
|
|
460
|
+
"worker: unhandled rejection",
|
|
461
|
+
reason instanceof Error ? reason.stack ?? reason.message : reason
|
|
462
|
+
]
|
|
463
|
+
});
|
|
464
|
+
});
|
|
455
465
|
const host = createWorkerHost(post);
|
|
456
466
|
const state = { core: void 0 };
|
|
457
467
|
port.on("message", (msg) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@dimforge/rapier2d-compat": "0.20.0",
|
|
37
37
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
38
38
|
"matter-js": "0.20.0",
|
|
39
|
-
"@irtio/protocol": "^4.
|
|
40
|
-
"@irtio/schema": "^4.
|
|
41
|
-
"@irtio/server": "^4.
|
|
39
|
+
"@irtio/protocol": "^4.2.0",
|
|
40
|
+
"@irtio/schema": "^4.2.0",
|
|
41
|
+
"@irtio/server": "^4.2.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/matter-js": "0.20.2"
|