@irtio/runtime 3.1.1 → 4.1.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-2L3RGWXM.js → chunk-KJN62J37.js} +131 -43
- package/dist/{chunk-XPRLQCNF.js → chunk-ZGJY4E3G.js} +1 -1
- package/dist/{contract-BoCNAPZT.d.ts → contract-DHWlURaq.d.ts} +20 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.js +4 -2
- package/dist/{room-CGbiPxi3.d.ts → room-Bpo_nqlO.d.ts} +15 -2
- package/dist/test/index.d.ts +16 -3
- package/dist/test/index.js +34 -7
- 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
|
|
@@ -578,12 +612,12 @@ import {
|
|
|
578
612
|
SERVER_OWNER as SERVER_OWNER2,
|
|
579
613
|
decodeFields,
|
|
580
614
|
encodeFields,
|
|
581
|
-
validateValue
|
|
615
|
+
validateValue as validateValue2
|
|
582
616
|
} from "@irtio/schema";
|
|
583
617
|
import { DenyError } from "@irtio/server";
|
|
584
618
|
|
|
585
619
|
// src/core/ownership.ts
|
|
586
|
-
import { SERVER_OWNER, defaultRecord } from "@irtio/schema";
|
|
620
|
+
import { SERVER_OWNER, cloneValue, defaultRecord, validateValue } from "@irtio/schema";
|
|
587
621
|
var HOLD_ACTIONS = /* @__PURE__ */ new Set([
|
|
588
622
|
"release",
|
|
589
623
|
"nearest",
|
|
@@ -735,7 +769,7 @@ function nearestConnected(core, collection, id) {
|
|
|
735
769
|
}
|
|
736
770
|
return best;
|
|
737
771
|
}
|
|
738
|
-
function addPerPlayerRecords(core, clientId) {
|
|
772
|
+
function addPerPlayerRecords(core, clientId, seeds) {
|
|
739
773
|
for (const c of core.ext.collections) {
|
|
740
774
|
if (c.kind !== "entity" || !c.perPlayer) continue;
|
|
741
775
|
const coll = plainEntity(core.plain, c.name);
|
|
@@ -746,7 +780,23 @@ function addPerPlayerRecords(core, clientId) {
|
|
|
746
780
|
continue;
|
|
747
781
|
}
|
|
748
782
|
try {
|
|
749
|
-
|
|
783
|
+
const record = defaultRecord(c);
|
|
784
|
+
const seed = seeds?.get(c.name);
|
|
785
|
+
if (seed && c.persist) {
|
|
786
|
+
for (const field of c.persist) {
|
|
787
|
+
const value = seed[field];
|
|
788
|
+
if (value === void 0) continue;
|
|
789
|
+
const desc = c.fields[c.fieldIndex.get(field) ?? -1];
|
|
790
|
+
if (!desc) continue;
|
|
791
|
+
const err = validateValue(desc.type, value, `${c.name}.${field}`);
|
|
792
|
+
if (err) {
|
|
793
|
+
core.log("warn", `persisted ${c.name}.${field} for ${clientId} dropped: ${err}`);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
record[field] = cloneValue(value);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
trackedEntity(core, c.name).add(clientId, record, { owner: clientId });
|
|
750
800
|
} catch (err) {
|
|
751
801
|
core.log("error", `perPlayer add on ${c.name} failed for ${clientId}:`, err);
|
|
752
802
|
}
|
|
@@ -942,7 +992,7 @@ function handleCall(core, clientId, payload) {
|
|
|
942
992
|
return true;
|
|
943
993
|
}
|
|
944
994
|
for (const f of desc.params) {
|
|
945
|
-
const problem =
|
|
995
|
+
const problem = validateValue2(f.type, params[f.name], f.name);
|
|
946
996
|
if (problem) {
|
|
947
997
|
traceEarly?.(errorText("E_RPC_BAD_PARAMS", problem));
|
|
948
998
|
sendReply(core, clientId, call.reqId, null, errorText("E_RPC_BAD_PARAMS", problem));
|
|
@@ -1136,10 +1186,10 @@ function descFor(core, name) {
|
|
|
1136
1186
|
}
|
|
1137
1187
|
return found;
|
|
1138
1188
|
}
|
|
1139
|
-
function sendCall(core, clientId, desc, params) {
|
|
1189
|
+
function sendCall(core, clientId, desc, params, noReply = false) {
|
|
1140
1190
|
const s = stateOf3(core);
|
|
1141
|
-
const reqId = s.nextReqId;
|
|
1142
|
-
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;
|
|
1143
1193
|
const bytes = encodeFields(desc.params, params);
|
|
1144
1194
|
core.send(
|
|
1145
1195
|
clientId,
|
|
@@ -1177,13 +1227,20 @@ function createCallProxy(core, clientId) {
|
|
|
1177
1227
|
if (typeof prop !== "string") return void 0;
|
|
1178
1228
|
return (params) => {
|
|
1179
1229
|
const desc = descFor(core, prop);
|
|
1180
|
-
|
|
1230
|
+
const p = new Promise((resolve, reject2) => {
|
|
1181
1231
|
callClient(core, clientId, desc, params ?? {}, {
|
|
1182
1232
|
timeoutMs: RPC_TIMEOUT_MS,
|
|
1183
1233
|
resolve,
|
|
1184
1234
|
reject: reject2
|
|
1185
1235
|
});
|
|
1186
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;
|
|
1187
1244
|
};
|
|
1188
1245
|
}
|
|
1189
1246
|
}
|
|
@@ -1200,7 +1257,7 @@ function createBroadcastProxy(core, except) {
|
|
|
1200
1257
|
for (const entry of core.clients.values()) {
|
|
1201
1258
|
if (!entry.connected) continue;
|
|
1202
1259
|
if (except?.has(entry.clientId)) continue;
|
|
1203
|
-
sendCall(core, entry.clientId, desc, params ?? {});
|
|
1260
|
+
sendCall(core, entry.clientId, desc, params ?? {}, true);
|
|
1204
1261
|
}
|
|
1205
1262
|
};
|
|
1206
1263
|
}
|
|
@@ -1223,6 +1280,7 @@ function handleReply(core, clientId, payload) {
|
|
|
1223
1280
|
const key = pendingKey(clientId, reply.reqId);
|
|
1224
1281
|
const pending = s.pending.get(key);
|
|
1225
1282
|
if (!pending) {
|
|
1283
|
+
if (isNoReplyReqId(reply.reqId)) return true;
|
|
1226
1284
|
core.log("warn", `REPLY from ${clientId} for unknown reqId ${reply.reqId}`);
|
|
1227
1285
|
return true;
|
|
1228
1286
|
}
|
|
@@ -1353,7 +1411,7 @@ function rejectAllHostCalls(core, reason) {
|
|
|
1353
1411
|
// src/core/writes.ts
|
|
1354
1412
|
import {
|
|
1355
1413
|
SERVER_OWNER as SERVER_OWNER3,
|
|
1356
|
-
cloneValue,
|
|
1414
|
+
cloneValue as cloneValue2,
|
|
1357
1415
|
decodeDelta,
|
|
1358
1416
|
defaultValue,
|
|
1359
1417
|
encodeDelta as encodeDelta2,
|
|
@@ -1407,7 +1465,7 @@ function applyMaskedPatch(fields, target, patch, mask) {
|
|
|
1407
1465
|
continue;
|
|
1408
1466
|
}
|
|
1409
1467
|
}
|
|
1410
|
-
target[f.name] =
|
|
1468
|
+
target[f.name] = cloneValue2(value);
|
|
1411
1469
|
}
|
|
1412
1470
|
}
|
|
1413
1471
|
function assignChanged(desc, target, key, prev, next) {
|
|
@@ -1487,7 +1545,7 @@ function applyOp(core, clientId, c, op) {
|
|
|
1487
1545
|
);
|
|
1488
1546
|
return;
|
|
1489
1547
|
}
|
|
1490
|
-
if (c.kind === "entity" && c
|
|
1548
|
+
if (c.kind === "entity" && isPerClient(c)) {
|
|
1491
1549
|
const coll = plainEntity(core.plain, c.name);
|
|
1492
1550
|
if (coll.has(op.id) && coll.ownerOf(op.id) !== clientId && !inClientView(core, clientId, c, op.id)) {
|
|
1493
1551
|
core.recordEvent("write-rejected", clientId, `${c.name}: ${op.id} is not in view`);
|
|
@@ -1532,17 +1590,17 @@ function applyOp(core, clientId, c, op) {
|
|
|
1532
1590
|
const fields = fieldList(c);
|
|
1533
1591
|
const growth = updateGrowth(c, leafPaths(fields, op.mask));
|
|
1534
1592
|
if (growth > 0 && !admitGrowth(core, clientId, growth)) return;
|
|
1535
|
-
const prev =
|
|
1536
|
-
const next =
|
|
1593
|
+
const prev = cloneValue2(current);
|
|
1594
|
+
const next = cloneValue2(current);
|
|
1537
1595
|
applyMaskedPatch(fields, next, op.patch, op.mask);
|
|
1538
|
-
const requested =
|
|
1596
|
+
const requested = cloneValue2(next);
|
|
1539
1597
|
if (c.physics || c.serverFields) {
|
|
1540
1598
|
const refused2 = [];
|
|
1541
1599
|
for (const leaf of leafPaths(fields, op.mask)) {
|
|
1542
1600
|
const field = leaf.names[0];
|
|
1543
1601
|
const why = ownerMayNotWrite(c, field);
|
|
1544
1602
|
if (why === void 0) continue;
|
|
1545
|
-
next[field] =
|
|
1603
|
+
next[field] = cloneValue2(prev[field]);
|
|
1546
1604
|
refused2.push(why);
|
|
1547
1605
|
}
|
|
1548
1606
|
if (refused2.length > 0) {
|
|
@@ -1580,7 +1638,7 @@ function applyOp(core, clientId, c, op) {
|
|
|
1580
1638
|
map = /* @__PURE__ */ new Map();
|
|
1581
1639
|
entry.accepted.set(key, map);
|
|
1582
1640
|
}
|
|
1583
|
-
for (const leaf of written) map.set(leafKey(leaf),
|
|
1641
|
+
for (const leaf of written) map.set(leafKey(leaf), cloneValue2(valueAt(result, leaf.names)));
|
|
1584
1642
|
}
|
|
1585
1643
|
const corrected = written.filter(
|
|
1586
1644
|
(leaf) => !deepEqual(valueAt(result, leaf.names), valueAt(requested, leaf.names))
|
|
@@ -3774,7 +3832,7 @@ import {
|
|
|
3774
3832
|
withBuiltins as withBuiltins2
|
|
3775
3833
|
} from "@irtio/protocol";
|
|
3776
3834
|
import {
|
|
3777
|
-
cloneValue as
|
|
3835
|
+
cloneValue as cloneValue3,
|
|
3778
3836
|
createDirtySet as createDirtySet3,
|
|
3779
3837
|
createState,
|
|
3780
3838
|
decodeSnapshot as decodeSnapshot3,
|
|
@@ -5121,7 +5179,7 @@ function stateOf6(core) {
|
|
|
5121
5179
|
function omniscientMemberships(core) {
|
|
5122
5180
|
const out = /* @__PURE__ */ new Map();
|
|
5123
5181
|
for (const desc of core.ext.collections) {
|
|
5124
|
-
if (desc
|
|
5182
|
+
if (!isPerClient(desc)) continue;
|
|
5125
5183
|
out.set(desc.name, new Set(plainEntity(core.plain, desc.name).ids()));
|
|
5126
5184
|
}
|
|
5127
5185
|
return out;
|
|
@@ -5130,9 +5188,7 @@ function hasRoleScoped(core) {
|
|
|
5130
5188
|
return core.ext.collections.some((c) => c.visibility === "role");
|
|
5131
5189
|
}
|
|
5132
5190
|
function hasSpatial(core) {
|
|
5133
|
-
return core.ext.collections.some(
|
|
5134
|
-
(c) => c.visibility === "spatial-grid"
|
|
5135
|
-
);
|
|
5191
|
+
return core.ext.collections.some(isPerClient);
|
|
5136
5192
|
}
|
|
5137
5193
|
function recorderSnapshot(core, state) {
|
|
5138
5194
|
if (state.view === "") {
|
|
@@ -6141,7 +6197,7 @@ function ownJoinCapture(plain, name, added, clientId) {
|
|
|
6141
6197
|
for (const id of added) {
|
|
6142
6198
|
const owner = coll.ownerOf(id);
|
|
6143
6199
|
if (name === PRESENCE_COLLECTION ? id !== clientId : owner !== clientId) continue;
|
|
6144
|
-
(mine ??= /* @__PURE__ */ new Map()).set(id, { value:
|
|
6200
|
+
(mine ??= /* @__PURE__ */ new Map()).set(id, { value: cloneValue3(coll.get(id)), owner });
|
|
6145
6201
|
}
|
|
6146
6202
|
return mine;
|
|
6147
6203
|
}
|
|
@@ -6176,6 +6232,7 @@ var RoomCore = class _RoomCore {
|
|
|
6176
6232
|
bytesOut: 0,
|
|
6177
6233
|
bytesOutByClient: /* @__PURE__ */ new Map(),
|
|
6178
6234
|
handlerErrors: 0,
|
|
6235
|
+
rpcClientFailures: 0,
|
|
6179
6236
|
encodesLastFlush: 0,
|
|
6180
6237
|
aoiEncodesLastFlush: 0,
|
|
6181
6238
|
gridBuildMs: 0,
|
|
@@ -6777,6 +6834,9 @@ var RoomCore = class _RoomCore {
|
|
|
6777
6834
|
role: this.resolveRole(options.role, void 0),
|
|
6778
6835
|
name: options.name ?? "",
|
|
6779
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,
|
|
6780
6840
|
deny: (reason) => {
|
|
6781
6841
|
throw new DenyError2(reason);
|
|
6782
6842
|
},
|
|
@@ -6838,7 +6898,7 @@ var RoomCore = class _RoomCore {
|
|
|
6838
6898
|
this.plain,
|
|
6839
6899
|
role2,
|
|
6840
6900
|
this.tick,
|
|
6841
|
-
|
|
6901
|
+
viewMemberships(this.ext, this.plain, clientId, role2)
|
|
6842
6902
|
)
|
|
6843
6903
|
};
|
|
6844
6904
|
}
|
|
@@ -6905,7 +6965,7 @@ var RoomCore = class _RoomCore {
|
|
|
6905
6965
|
this.clients.set(clientId, entry);
|
|
6906
6966
|
this.invalidateClients();
|
|
6907
6967
|
reclaimHolds(this.internals, clientId, entry.playerId);
|
|
6908
|
-
addPerPlayerRecords(this.internals, clientId);
|
|
6968
|
+
addPerPlayerRecords(this.internals, clientId, options.persisted);
|
|
6909
6969
|
const onJoin = this.definition.config.onJoin;
|
|
6910
6970
|
if (onJoin) {
|
|
6911
6971
|
const ctx = this.ctxFor(clientId, reconnecting);
|
|
@@ -6913,7 +6973,7 @@ var RoomCore = class _RoomCore {
|
|
|
6913
6973
|
}
|
|
6914
6974
|
if (!entry.npc) this.loop.noteActivity();
|
|
6915
6975
|
this.recordEvent("join", clientId, reconnecting ? "reconnect" : void 0);
|
|
6916
|
-
const memberships =
|
|
6976
|
+
const memberships = viewMemberships(this.ext, this.plain, clientId, entry.role);
|
|
6917
6977
|
entry.spatialMembership = memberships;
|
|
6918
6978
|
const snapshot = encodeViewSnapshot(this.ext, this.plain, entry.role, this.tick, memberships);
|
|
6919
6979
|
entry.welcomed = true;
|
|
@@ -6928,7 +6988,7 @@ var RoomCore = class _RoomCore {
|
|
|
6928
6988
|
const captured = mine ?? /* @__PURE__ */ new Map();
|
|
6929
6989
|
for (const id of cd.added) {
|
|
6930
6990
|
if (!visible.has(id) || captured.has(id)) continue;
|
|
6931
|
-
captured.set(id, { value:
|
|
6991
|
+
captured.set(id, { value: cloneValue3(coll.get(id)), owner: coll.ownerOf(id) });
|
|
6932
6992
|
}
|
|
6933
6993
|
if (captured.size > 0) pending.set(name2, captured);
|
|
6934
6994
|
} else if (mine) pending.set(name2, mine);
|
|
@@ -6969,6 +7029,30 @@ var RoomCore = class _RoomCore {
|
|
|
6969
7029
|
this.invalidateClients();
|
|
6970
7030
|
this.eventFlush();
|
|
6971
7031
|
}
|
|
7032
|
+
// ---- M8 proposal 13: identity-keyed player persistence ----
|
|
7033
|
+
/**
|
|
7034
|
+
* The current values of every `persist` field this client's `perPlayer` records hold, or
|
|
7035
|
+
* `undefined` when the schema declares none and when the client has no records.
|
|
7036
|
+
*
|
|
7037
|
+
* Read by the relay host just before `leave`, which is the last moment the records exist: the
|
|
7038
|
+
* declared leave rules remove a `perPlayer` record.
|
|
7039
|
+
*/
|
|
7040
|
+
persistedFieldsOf(clientId) {
|
|
7041
|
+
let out;
|
|
7042
|
+
for (const c of this.ext.collections) {
|
|
7043
|
+
if (c.kind !== "entity" || !c.perPlayer || !c.persist || c.persist.length === 0) continue;
|
|
7044
|
+
const record = plainEntity(this.plain, c.name).get(clientId);
|
|
7045
|
+
if (!record) continue;
|
|
7046
|
+
const picked = {};
|
|
7047
|
+
for (const field of c.persist) {
|
|
7048
|
+
const value = record[field];
|
|
7049
|
+
if (value !== void 0) picked[field] = cloneValue3(value);
|
|
7050
|
+
}
|
|
7051
|
+
if (Object.keys(picked).length > 0) (out ??= /* @__PURE__ */ new Map()).set(c.name, picked);
|
|
7052
|
+
}
|
|
7053
|
+
return out;
|
|
7054
|
+
}
|
|
7055
|
+
// ---- end M8 proposal 13 ----
|
|
6972
7056
|
markDisconnected(clientId) {
|
|
6973
7057
|
const entry = this.clients.get(clientId);
|
|
6974
7058
|
if (!entry) {
|
|
@@ -6995,6 +7079,9 @@ var RoomCore = class _RoomCore {
|
|
|
6995
7079
|
// other entry point (join, leave, write, ownership) has no client tick by construction.
|
|
6996
7080
|
clientTick: void 0,
|
|
6997
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,
|
|
6998
7085
|
room: this.room,
|
|
6999
7086
|
// M7 pf4 (a): on every ctx rather than only the RPC one, because the type says it is there.
|
|
7000
7087
|
// `rpc.ts` is what turns the throw into an `ok: false` reply; anywhere else it is a throw.
|
|
@@ -7193,7 +7280,7 @@ var RoomCore = class _RoomCore {
|
|
|
7193
7280
|
this.ledger?.newFlush();
|
|
7194
7281
|
this.stats.encodesLastFlush = 0;
|
|
7195
7282
|
this.stats.aoiEncodesLastFlush = 0;
|
|
7196
|
-
const spatialDescs = this.ext
|
|
7283
|
+
const { spatial: spatialDescs, perClient } = perClientDescs(this.ext);
|
|
7197
7284
|
const buildStarted = performance.now();
|
|
7198
7285
|
const indexes = buildSpatialIndexes(spatialDescs, this.plain);
|
|
7199
7286
|
this.stats.gridBuildMs += performance.now() - buildStarted;
|
|
@@ -7201,7 +7288,7 @@ var RoomCore = class _RoomCore {
|
|
|
7201
7288
|
for (const entry of this.clients.values()) {
|
|
7202
7289
|
if (entry.connected) {
|
|
7203
7290
|
const queryStarted = performance.now();
|
|
7204
|
-
const memberships =
|
|
7291
|
+
const memberships = viewMemberships(
|
|
7205
7292
|
this.ext,
|
|
7206
7293
|
this.plain,
|
|
7207
7294
|
entry.clientId,
|
|
@@ -7244,7 +7331,7 @@ var RoomCore = class _RoomCore {
|
|
|
7244
7331
|
}
|
|
7245
7332
|
}
|
|
7246
7333
|
let delta;
|
|
7247
|
-
if (
|
|
7334
|
+
if (perClient.length > 0) {
|
|
7248
7335
|
const sourceDirty = entry.pendingJoinAdds ? stripAdds(this.plain, dirty, entry.pendingJoinAdds) : dirty;
|
|
7249
7336
|
let shared;
|
|
7250
7337
|
if (entry.pendingJoinAdds) {
|
|
@@ -7269,7 +7356,7 @@ var RoomCore = class _RoomCore {
|
|
|
7269
7356
|
this.ledger ? { shared } : void 0
|
|
7270
7357
|
);
|
|
7271
7358
|
}
|
|
7272
|
-
const aoiDirty =
|
|
7359
|
+
const aoiDirty = perClientViewDirty(
|
|
7273
7360
|
this.ext,
|
|
7274
7361
|
sourceDirty,
|
|
7275
7362
|
entry.spatialMembership,
|
|
@@ -7310,7 +7397,7 @@ var RoomCore = class _RoomCore {
|
|
|
7310
7397
|
encodeFrame5(FrameType6.DELTA, delta),
|
|
7311
7398
|
this.ledger ? {
|
|
7312
7399
|
...churn ? { churn } : {},
|
|
7313
|
-
...
|
|
7400
|
+
...perClient.length > 0 ? {} : { shared: delta }
|
|
7314
7401
|
} : void 0
|
|
7315
7402
|
);
|
|
7316
7403
|
}
|
|
@@ -7349,6 +7436,7 @@ export {
|
|
|
7349
7436
|
summarize,
|
|
7350
7437
|
RpcTraceRing,
|
|
7351
7438
|
isVisible,
|
|
7439
|
+
isPerClient,
|
|
7352
7440
|
visibleTo,
|
|
7353
7441
|
visibleNames,
|
|
7354
7442
|
viewKeyFor,
|
|
@@ -552,6 +552,12 @@ interface JoinOptions {
|
|
|
552
552
|
* therefore the only thing `room.ban` can key a cooldown on honestly.
|
|
553
553
|
*/
|
|
554
554
|
readonly subject?: string;
|
|
555
|
+
/**
|
|
556
|
+
* Seed values for this client's fresh `perPlayer` records, from the relay host's persistence
|
|
557
|
+
* blob: collection name → partial field values. Applied only where a new record is created; a
|
|
558
|
+
* reclaimed or reconnected record keeps what it has.
|
|
559
|
+
*/
|
|
560
|
+
readonly persisted?: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
|
|
555
561
|
}
|
|
556
562
|
interface JoinResult {
|
|
557
563
|
readonly tick: number;
|
|
@@ -598,6 +604,13 @@ interface RoomStats {
|
|
|
598
604
|
/** Per connected client. */
|
|
599
605
|
bytesOutByClient: Map<string, number>;
|
|
600
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;
|
|
601
614
|
encodesLastFlush: number;
|
|
602
615
|
aoiEncodesLastFlush: number;
|
|
603
616
|
gridBuildMs: number;
|
|
@@ -657,6 +670,13 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
|
|
|
657
670
|
leave(clientId: string, reason: LeaveReason): void;
|
|
658
671
|
/** Grace window opened by the host: presence `connected = false`; frames are no longer sent. */
|
|
659
672
|
markDisconnected(clientId: string): void;
|
|
673
|
+
/**
|
|
674
|
+
* M8 proposal 13: the current values of every `persist` field this client's `perPlayer` records
|
|
675
|
+
* hold, or `undefined` when the schema declares none and when the client has no records.
|
|
676
|
+
*
|
|
677
|
+
* Read by the relay host just before `leave`, to write the player's persistence blob.
|
|
678
|
+
*/
|
|
679
|
+
persistedFieldsOf(clientId: string): ReadonlyMap<string, Record<string, unknown>> | undefined;
|
|
660
680
|
/** One inbound protocol frame (`WRITE` | `CALL` | `REPLY` | `MSG`). Anything else → host.kick(E_BAD_FRAME). */
|
|
661
681
|
receive(clientId: string, frame: Uint8Array): void;
|
|
662
682
|
/** Full state + tick + rng for hibernation. Runs `onSleep`. Clears timers. */
|
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. */
|
|
@@ -1152,6 +1157,14 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
1152
1157
|
/** Event mode only: presence/lifecycle changes are their own event. No-op in tick mode. */
|
|
1153
1158
|
private eventFlush;
|
|
1154
1159
|
leave(clientId: string, reason: LeaveReason): void;
|
|
1160
|
+
/**
|
|
1161
|
+
* The current values of every `persist` field this client's `perPlayer` records hold, or
|
|
1162
|
+
* `undefined` when the schema declares none and when the client has no records.
|
|
1163
|
+
*
|
|
1164
|
+
* Read by the relay host just before `leave`, which is the last moment the records exist: the
|
|
1165
|
+
* declared leave rules remove a `perPlayer` record.
|
|
1166
|
+
*/
|
|
1167
|
+
persistedFieldsOf(clientId: string): ReadonlyMap<string, Record<string, unknown>> | undefined;
|
|
1155
1168
|
markDisconnected(clientId: string): void;
|
|
1156
1169
|
ctxFor(clientId: string, reconnecting?: boolean): Ctx;
|
|
1157
1170
|
/**
|
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
|
/**
|
|
@@ -20,6 +20,14 @@ declare class FakeClock {
|
|
|
20
20
|
clearTimeout(handle: unknown): void;
|
|
21
21
|
/** Number of timers still armed (a room that is ticking always has at least one). */
|
|
22
22
|
get pending(): number;
|
|
23
|
+
/**
|
|
24
|
+
* The `at` of the earliest armed timer, or `undefined` when nothing is armed. Read-only: it
|
|
25
|
+
* changes nothing. It exists for callers that need to advance to the next timer rather than
|
|
26
|
+
* over it — `testRoom`'s `t.run(ms)` uses it to hop timer by timer, so a script parked on an
|
|
27
|
+
* `await` (whose continuation only runs once `advance` has returned and microtasks drain) can
|
|
28
|
+
* arm its next timer and keep going inside a single long run.
|
|
29
|
+
*/
|
|
30
|
+
nextDueAt(): number | undefined;
|
|
23
31
|
/**
|
|
24
32
|
* Advances to `time + ms`, firing every timer that comes due in order (earliest `at` first,
|
|
25
33
|
* ties broken by creation order). A timer that arms another timer inside the window fires too.
|
|
@@ -361,6 +369,11 @@ interface JoinSpec {
|
|
|
361
369
|
* `onAdmit` that requires identity needs to be tested against.
|
|
362
370
|
*/
|
|
363
371
|
readonly subject?: string;
|
|
372
|
+
/**
|
|
373
|
+
* The persistence blob this join arrives with: collection name → persisted field values. What
|
|
374
|
+
* the relay host reads from the store for an identified player, without a store or a socket.
|
|
375
|
+
*/
|
|
376
|
+
readonly persisted?: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
|
|
364
377
|
}
|
|
365
378
|
interface UntilOptions {
|
|
366
379
|
/** Default 1000. */
|
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 {
|
|
@@ -28,6 +29,22 @@ var FakeClock = class {
|
|
|
28
29
|
get pending() {
|
|
29
30
|
return this.timers.size;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The `at` of the earliest armed timer, or `undefined` when nothing is armed. Read-only: it
|
|
34
|
+
* changes nothing. It exists for callers that need to advance to the next timer rather than
|
|
35
|
+
* over it — `testRoom`'s `t.run(ms)` uses it to hop timer by timer, so a script parked on an
|
|
36
|
+
* `await` (whose continuation only runs once `advance` has returned and microtasks drain) can
|
|
37
|
+
* arm its next timer and keep going inside a single long run.
|
|
38
|
+
*/
|
|
39
|
+
nextDueAt() {
|
|
40
|
+
let earliest;
|
|
41
|
+
for (const t of this.timers.values()) {
|
|
42
|
+
if (!earliest || t.at < earliest.at || t.at === earliest.at && t.id < earliest.id) {
|
|
43
|
+
earliest = t;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return earliest?.at;
|
|
47
|
+
}
|
|
31
48
|
/**
|
|
32
49
|
* Advances to `time + ms`, firing every timer that comes due in order (earliest `at` first,
|
|
33
50
|
* ties broken by creation order). A timer that arms another timer inside the window fires too.
|
|
@@ -336,6 +353,7 @@ import {
|
|
|
336
353
|
encodeFrame,
|
|
337
354
|
encodeMsg,
|
|
338
355
|
encodeReply,
|
|
356
|
+
isNoReplyReqId,
|
|
339
357
|
rpcTable
|
|
340
358
|
} from "@irtio/protocol";
|
|
341
359
|
import {
|
|
@@ -605,7 +623,7 @@ var FakeClientImpl = class {
|
|
|
605
623
|
/** Flags any collection the frame named that this client's role may not see. */
|
|
606
624
|
checkFrameVisibility(delta) {
|
|
607
625
|
for (const dc of delta.collections) {
|
|
608
|
-
const spatial = this.bridge.ext.collection(dc.name)
|
|
626
|
+
const spatial = isPerClient(this.bridge.ext.collection(dc.name));
|
|
609
627
|
const leaked = dc.ops.filter((op) => {
|
|
610
628
|
if (!this.bridge.visible(this.role, this.id, dc.name)) return true;
|
|
611
629
|
if (!spatial) return false;
|
|
@@ -644,20 +662,26 @@ var FakeClientImpl = class {
|
|
|
644
662
|
this.calls.push({ name: desc.name, params });
|
|
645
663
|
const impl = this.impls.get(desc.name);
|
|
646
664
|
if (!impl) return;
|
|
665
|
+
const noReply = isNoReplyReqId(call.reqId);
|
|
647
666
|
let result;
|
|
648
667
|
try {
|
|
649
668
|
result = impl(params);
|
|
650
669
|
} catch (err) {
|
|
651
|
-
this.replyError(call.reqId, err);
|
|
670
|
+
if (!noReply) this.replyError(call.reqId, err);
|
|
652
671
|
return;
|
|
653
672
|
}
|
|
654
673
|
if (isThenable(result)) {
|
|
655
674
|
Promise.resolve(result).then(
|
|
656
|
-
(v) =>
|
|
657
|
-
|
|
675
|
+
(v) => {
|
|
676
|
+
if (!noReply) this.replyOk(desc, call.reqId, v);
|
|
677
|
+
},
|
|
678
|
+
(err) => {
|
|
679
|
+
if (!noReply) this.replyError(call.reqId, err);
|
|
680
|
+
}
|
|
658
681
|
);
|
|
659
682
|
return;
|
|
660
683
|
}
|
|
684
|
+
if (noReply) return;
|
|
661
685
|
this.replyOk(desc, call.reqId, result);
|
|
662
686
|
}
|
|
663
687
|
replyOk(desc, reqId, result) {
|
|
@@ -966,8 +990,11 @@ var Harness = class {
|
|
|
966
990
|
...spec.playerId !== void 0 ? { playerId: spec.playerId } : {},
|
|
967
991
|
// ---- end M6 lane K ----
|
|
968
992
|
// ---- M8 lane 5: moderation ----
|
|
969
|
-
...spec.subject !== void 0 ? { subject: spec.subject } : {}
|
|
993
|
+
...spec.subject !== void 0 ? { subject: spec.subject } : {},
|
|
970
994
|
// ---- end M8 lane 5 ----
|
|
995
|
+
// ---- M8 proposal 13 ----
|
|
996
|
+
...spec.persisted !== void 0 ? { persisted: spec.persisted } : {}
|
|
997
|
+
// ---- end M8 proposal 13 ----
|
|
971
998
|
};
|
|
972
999
|
const client = new FakeClientImpl(this.bridge, id, options);
|
|
973
1000
|
this.byId.set(id, client);
|
|
@@ -991,7 +1018,7 @@ var Harness = class {
|
|
|
991
1018
|
const keep = visibleNames(ext, client.role);
|
|
992
1019
|
const policy = createVisibilityPolicy(ext, this.coreRef.plain, client.id, client.role);
|
|
993
1020
|
for (const c of ext.collections) {
|
|
994
|
-
if (c
|
|
1021
|
+
if (isPerClient(c)) {
|
|
995
1022
|
const coll = client.view[c.name];
|
|
996
1023
|
const ids = coll ? [...coll.ids()].filter((id) => !policy.maySeeEntity(c, id)) : [];
|
|
997
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": "
|
|
3
|
+
"version": "4.1.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": "
|
|
40
|
-
"@irtio/schema": "
|
|
41
|
-
"@irtio/server": "
|
|
39
|
+
"@irtio/protocol": "^4.1.0",
|
|
40
|
+
"@irtio/schema": "^4.1.0",
|
|
41
|
+
"@irtio/server": "^4.1.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/matter-js": "0.20.2"
|