@irtio/runtime 0.11.0 → 3.0.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-ACC3SDIO.js → chunk-3BCTP4YS.js} +79 -6
- package/dist/{chunk-GYPUNTQN.js → chunk-NPRIYYNM.js} +3011 -1384
- package/dist/{contract-Hk4SkDtW.d.ts → contract-BoCNAPZT.d.ts} +190 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +14 -2
- package/dist/{room-0FZBGyke.d.ts → room-CGbiPxi3.d.ts} +334 -12
- package/dist/test/index.d.ts +26 -4
- package/dist/test/index.js +26 -4
- package/dist/worker/index.d.ts +77 -11
- package/dist/worker/index.js +66 -8
- package/package.json +9 -4
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { L as LogLevel, T as TimelineDump,
|
|
2
|
-
import { ErrorCodeName, ProfileSnapshot } from '@irtio/protocol';
|
|
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-BoCNAPZT.js';
|
|
2
|
+
import { ErrorCodeName, ProfileSnapshot, PlatformNotice } from '@irtio/protocol';
|
|
3
3
|
import { NpcConfig, LeaveReason } from '@irtio/server';
|
|
4
4
|
import '@irtio/schema';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Supervisor ↔ room-worker `postMessage` protocol. Every `Uint8Array` is transferred.
|
|
8
|
-
* Keep it additive: deployed supervisors speak exactly this.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
6
|
/**
|
|
12
7
|
* One deployment in a wake-time migration chain. The worker imports each
|
|
13
8
|
* `migrationUrl` and runs `up` inside its own sandbox — migrations are tenant code and never run
|
|
@@ -40,6 +35,11 @@ type ToWorker = {
|
|
|
40
35
|
* existing supervisor already sends, and a room without it pays nothing.
|
|
41
36
|
*/
|
|
42
37
|
profile?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* M7 pf4: run this room with an RPC trace ring. Absent means off — the shape every existing
|
|
40
|
+
* supervisor already sends — and a room without it pays nothing. `irtio dev --trace-rpc`.
|
|
41
|
+
*/
|
|
42
|
+
traceRpc?: boolean;
|
|
43
43
|
} | {
|
|
44
44
|
t: 'join';
|
|
45
45
|
clientId: string;
|
|
@@ -51,6 +51,9 @@ type ToWorker = {
|
|
|
51
51
|
reconnecting?: boolean;
|
|
52
52
|
/** D44: this join is a scripted NPC's loopback session. */
|
|
53
53
|
npc?: boolean;
|
|
54
|
+
/** M8 lane 5: the verified subject, absent for a key join. See `JoinOptions.subject` — it is
|
|
55
|
+
* deliberately narrower than `playerId`, which always has a value. */
|
|
56
|
+
subject?: string;
|
|
54
57
|
} | {
|
|
55
58
|
t: 'leave';
|
|
56
59
|
clientId: string;
|
|
@@ -63,12 +66,23 @@ type ToWorker = {
|
|
|
63
66
|
clientId: string;
|
|
64
67
|
bytes: Uint8Array;
|
|
65
68
|
}
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
69
|
+
/**
|
|
70
|
+
* `forSave` (D24) asks for the bytes only: no `onSleep`, no timers cleared, nothing rejected —
|
|
71
|
+
* the room keeps running. `false` is the departure: `onSleep` runs, timers are cleared, pending
|
|
72
|
+
* work is rejected, and the worker is about to be terminated.
|
|
73
|
+
*
|
|
74
|
+
* **Required, deliberately** (bugs.md #82). It was optional, defaulting to the departure, and
|
|
75
|
+
* the periodic `snapshotEveryMs` snapshot of a *running* event-mode room simply omitted it — so
|
|
76
|
+
* every such room ran `onSleep` and lost its armed timers every thirty seconds while it kept
|
|
77
|
+
* running. Only the caller knows which of the two it meant, so the protocol makes it say. Host
|
|
78
|
+
* and worker are one shipping unit (`@irtio/supervisor` is private and depends on
|
|
79
|
+
* `@irtio/runtime` by `workspace:*`, and this module is not on `@irtio/runtime`'s export map),
|
|
80
|
+
* so there is no mixed-version pair for the missing field to have to mean anything to.
|
|
81
|
+
*/
|
|
68
82
|
| {
|
|
69
83
|
t: 'serialize';
|
|
70
84
|
reqId: number;
|
|
71
|
-
forSave
|
|
85
|
+
forSave: boolean;
|
|
72
86
|
} | {
|
|
73
87
|
t: 'inspect';
|
|
74
88
|
reqId: number;
|
|
@@ -132,6 +146,17 @@ type ToWorker = {
|
|
|
132
146
|
name: string;
|
|
133
147
|
from: string;
|
|
134
148
|
payload: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* M7 D-P2: the platform has something to tell this room's players, and the room gets the first
|
|
152
|
+
* look. Carries a `reqId` because the answer decides whether the supervisor broadcasts, so this
|
|
153
|
+
* is a request and not a notification — a rare thing on this side of the wire, and the reason is
|
|
154
|
+
* that a veto only means anything before the frame goes out.
|
|
155
|
+
*/
|
|
156
|
+
| {
|
|
157
|
+
t: 'platformNotice';
|
|
158
|
+
reqId: number;
|
|
159
|
+
notice: PlatformNotice;
|
|
135
160
|
} | {
|
|
136
161
|
t: 'stop';
|
|
137
162
|
};
|
|
@@ -204,6 +229,17 @@ type FromWorker = {
|
|
|
204
229
|
t: 'serialized';
|
|
205
230
|
reqId: number;
|
|
206
231
|
bytes: Uint8Array;
|
|
232
|
+
/**
|
|
233
|
+
* The earliest pending `room.setTimeout`/`setInterval` deadline the departure serialize
|
|
234
|
+
* just cleared, in **wall-clock ms** (`Date.now()`'s frame, translated in the worker the
|
|
235
|
+
* same way `setAlarm`'s `atMs` is — the room's own clock is `performance.now()` and means
|
|
236
|
+
* nothing to a supervisor that outlives the worker).
|
|
237
|
+
*
|
|
238
|
+
* Absent when the room had no timers armed, and always absent for `forSave`, which clears
|
|
239
|
+
* nothing and therefore loses nothing. The supervisor arms an internal wake for it so the
|
|
240
|
+
* room is up again by the time its timer would have fired.
|
|
241
|
+
*/
|
|
242
|
+
nextTimerAt?: number;
|
|
207
243
|
} | {
|
|
208
244
|
t: 'inspected';
|
|
209
245
|
reqId: number;
|
|
@@ -217,6 +253,13 @@ type FromWorker = {
|
|
|
217
253
|
}[];
|
|
218
254
|
/** Worker process RSS in bytes. */
|
|
219
255
|
rss: number;
|
|
256
|
+
/**
|
|
257
|
+
* M7 pf4: the RPC trace and the per-client stamp-lag table, present only when this room was
|
|
258
|
+
* armed with `traceRpc`. It rides `inspected` rather than a message of its own because the
|
|
259
|
+
* dev page already polls `inspect` once a second and a second round trip would buy nothing
|
|
260
|
+
* — and because a new worker message is a wider change than an additive field on one.
|
|
261
|
+
*/
|
|
262
|
+
rpcTrace?: RpcTraceDump;
|
|
220
263
|
}
|
|
221
264
|
/** D41: the recorder is armed. `ok: false` when there is no room core to arm. */
|
|
222
265
|
| {
|
|
@@ -224,6 +267,17 @@ type FromWorker = {
|
|
|
224
267
|
reqId: number;
|
|
225
268
|
ok: boolean;
|
|
226
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* M7 D-P2: the room's answer to a `platformNotice`. `broadcast: false` only when the handler
|
|
272
|
+
* returned exactly `false`. A handler that threw, a room with no handler, and a room whose core
|
|
273
|
+
* is not up all answer `true`, because the veto is a room saying "I will tell them myself" and
|
|
274
|
+
* none of those three said anything.
|
|
275
|
+
*/
|
|
276
|
+
| {
|
|
277
|
+
t: 'platformNoticeResult';
|
|
278
|
+
reqId: number;
|
|
279
|
+
broadcast: boolean;
|
|
280
|
+
}
|
|
227
281
|
/** D41: the recorded timeline. `dump` absent when nobody armed the recorder. */
|
|
228
282
|
| {
|
|
229
283
|
t: 'timeline';
|
|
@@ -368,5 +422,17 @@ interface WorkerState {
|
|
|
368
422
|
* killing the worker.
|
|
369
423
|
*/
|
|
370
424
|
declare function handleMessage(state: WorkerState, host: RoomHost, post: (msg: FromWorker, transfer?: ArrayBuffer[]) => void, msg: ToWorker): Promise<void>;
|
|
425
|
+
/**
|
|
426
|
+
* Replaces `Math.random` with sfc32 seeded from 16 host-supplied bytes.
|
|
427
|
+
*
|
|
428
|
+
* Box day 2 (2026-09-08) measured why this exists: two VMs restored from one golden snapshot
|
|
429
|
+
* handed their room workers **bit-for-bit identical `Math.random` streams** — V8 seeds a new
|
|
430
|
+
* isolate's PRNG from process state that a memory snapshot clones, and the kernel's VMGenID
|
|
431
|
+
* reseed does not reach it. `Math.random` is the only random primitive room code has (the
|
|
432
|
+
* sandbox refuses `node:crypto`), so an unseeded clone gives two different projects the same
|
|
433
|
+
* dice. The seed comes from the supervisor's clone-safe entropy mixer via `workerData`, so
|
|
434
|
+
* every worker — cold boot or restore, same VM or sibling clones — rolls its own.
|
|
435
|
+
*/
|
|
436
|
+
declare function seedMathRandom(seedHex: string): void;
|
|
371
437
|
|
|
372
|
-
export { type FromWorker, type MigrationChainStep, type ToWorker, type WorkerState, createWorkerHost, handleMessage, transferList };
|
|
438
|
+
export { type FromWorker, type MigrationChainStep, type ToWorker, type WorkerState, createWorkerHost, handleMessage, seedMathRandom, transferList };
|
package/dist/worker/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-3BCTP4YS.js";
|
|
4
4
|
import {
|
|
5
|
+
AdmissionError,
|
|
5
6
|
RoomCore,
|
|
6
7
|
RoomFullError,
|
|
7
8
|
initMatter,
|
|
@@ -9,11 +10,11 @@ import {
|
|
|
9
10
|
initRapier2d,
|
|
10
11
|
onFirstRapierStep,
|
|
11
12
|
rapierHasStepped
|
|
12
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-NPRIYYNM.js";
|
|
13
14
|
|
|
14
15
|
// src/worker/index.ts
|
|
15
16
|
import { getHeapStatistics } from "v8";
|
|
16
|
-
import { isMainThread, parentPort } from "worker_threads";
|
|
17
|
+
import { isMainThread, parentPort, workerData } from "worker_threads";
|
|
17
18
|
import { isRoomDefinition } from "@irtio/server";
|
|
18
19
|
|
|
19
20
|
// src/worker/protocol.ts
|
|
@@ -196,7 +197,8 @@ async function handleMessage(state, host, post, msg) {
|
|
|
196
197
|
roomId: msg.roomId,
|
|
197
198
|
...msg.seed !== void 0 ? { seed: msg.seed } : {},
|
|
198
199
|
...msg.publicUrl !== void 0 ? { publicUrl: msg.publicUrl } : {},
|
|
199
|
-
...msg.profile === true ? { profile: true } : {}
|
|
200
|
+
...msg.profile === true ? { profile: true } : {},
|
|
201
|
+
...msg.traceRpc === true ? { traceRpc: true } : {}
|
|
200
202
|
};
|
|
201
203
|
const core = snapshot ? RoomCore.restore(def, snapshot, host, options) : new RoomCore(def, host, options);
|
|
202
204
|
state.core = core;
|
|
@@ -259,7 +261,10 @@ async function handleMessage(state, host, post, msg) {
|
|
|
259
261
|
...msg.name !== void 0 ? { name: msg.name } : {},
|
|
260
262
|
...msg.playerId !== void 0 ? { playerId: msg.playerId } : {},
|
|
261
263
|
...msg.reconnecting !== void 0 ? { reconnecting: msg.reconnecting } : {},
|
|
262
|
-
...msg.npc === true ? { npc: true } : {}
|
|
264
|
+
...msg.npc === true ? { npc: true } : {},
|
|
265
|
+
// M8 lane 5: absent for a key join, and absent is the answer `onAdmit` and `room.ban`
|
|
266
|
+
// both need to be able to see.
|
|
267
|
+
...msg.subject !== void 0 ? { subject: msg.subject } : {}
|
|
263
268
|
};
|
|
264
269
|
const result = core.join(msg.clientId, joinOptions);
|
|
265
270
|
const snapshot = ownBuffer(result.snapshot);
|
|
@@ -276,6 +281,13 @@ async function handleMessage(state, host, post, msg) {
|
|
|
276
281
|
} catch (err) {
|
|
277
282
|
if (err instanceof RoomFullError) {
|
|
278
283
|
post({ t: "joinRejected", clientId: msg.clientId, code: "E_ROOM_FULL" });
|
|
284
|
+
} else if (err instanceof AdmissionError) {
|
|
285
|
+
post({
|
|
286
|
+
t: "joinRejected",
|
|
287
|
+
clientId: msg.clientId,
|
|
288
|
+
code: "E_ADMISSION",
|
|
289
|
+
reason: err.reason
|
|
290
|
+
});
|
|
279
291
|
} else {
|
|
280
292
|
post({
|
|
281
293
|
t: "joinRejected",
|
|
@@ -302,8 +314,17 @@ async function handleMessage(state, host, post, msg) {
|
|
|
302
314
|
case "serialize": {
|
|
303
315
|
const core = state.core;
|
|
304
316
|
if (!core) return;
|
|
305
|
-
const bytes = ownBuffer(msg.forSave
|
|
306
|
-
|
|
317
|
+
const bytes = ownBuffer(msg.forSave ? core.snapshot() : core.serialize());
|
|
318
|
+
const timerAt = msg.forSave ? void 0 : core.lastSerializeTimerAt;
|
|
319
|
+
post(
|
|
320
|
+
{
|
|
321
|
+
t: "serialized",
|
|
322
|
+
reqId: msg.reqId,
|
|
323
|
+
bytes,
|
|
324
|
+
...timerAt === void 0 ? {} : { nextTimerAt: Date.now() + (timerAt - performance.now()) }
|
|
325
|
+
},
|
|
326
|
+
[bytes.buffer]
|
|
327
|
+
);
|
|
307
328
|
return;
|
|
308
329
|
}
|
|
309
330
|
case "inspect": {
|
|
@@ -316,7 +337,13 @@ async function handleMessage(state, host, post, msg) {
|
|
|
316
337
|
tick: ins.tick,
|
|
317
338
|
stateJson: ins.state,
|
|
318
339
|
recent: ins.recent,
|
|
319
|
-
rss: typeof process !== "undefined" ? process.memoryUsage().rss : 0
|
|
340
|
+
rss: typeof process !== "undefined" ? process.memoryUsage().rss : 0,
|
|
341
|
+
// M7 pf4: absent unless this room was armed, which is what makes `undefined` mean
|
|
342
|
+
// "nobody asked" rather than "nobody called".
|
|
343
|
+
...(() => {
|
|
344
|
+
const trace = core.rpcTrace();
|
|
345
|
+
return trace === void 0 ? {} : { rpcTrace: trace };
|
|
346
|
+
})()
|
|
320
347
|
});
|
|
321
348
|
return;
|
|
322
349
|
}
|
|
@@ -352,6 +379,11 @@ async function handleMessage(state, host, post, msg) {
|
|
|
352
379
|
state.core?.fireAlarm(msg.name);
|
|
353
380
|
return;
|
|
354
381
|
}
|
|
382
|
+
case "platformNotice": {
|
|
383
|
+
const broadcast = state.core?.deliverPlatformNotice(msg.notice) ?? true;
|
|
384
|
+
post({ t: "platformNoticeResult", reqId: msg.reqId, broadcast });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
355
387
|
case "busEvent": {
|
|
356
388
|
state.core?.deliverBusEvent(msg.channel, msg.from, msg.payload);
|
|
357
389
|
return;
|
|
@@ -390,7 +422,32 @@ async function handleMessage(state, host, post, msg) {
|
|
|
390
422
|
});
|
|
391
423
|
}
|
|
392
424
|
}
|
|
425
|
+
function seedMathRandom(seedHex) {
|
|
426
|
+
if (!/^[0-9a-f]{32}$/.test(seedHex)) return;
|
|
427
|
+
const u32 = (i) => Number.parseInt(seedHex.slice(i * 8, i * 8 + 8), 16) >>> 0;
|
|
428
|
+
let a = u32(0);
|
|
429
|
+
let b = u32(1);
|
|
430
|
+
let c = u32(2);
|
|
431
|
+
let d = u32(3);
|
|
432
|
+
const sfc32 = () => {
|
|
433
|
+
a >>>= 0;
|
|
434
|
+
b >>>= 0;
|
|
435
|
+
c >>>= 0;
|
|
436
|
+
d >>>= 0;
|
|
437
|
+
const t = a + b + d >>> 0;
|
|
438
|
+
d = d + 1 >>> 0;
|
|
439
|
+
a = b ^ b >>> 9;
|
|
440
|
+
b = c + (c << 3) >>> 0;
|
|
441
|
+
c = (c << 21 | c >>> 11) >>> 0;
|
|
442
|
+
c = c + t >>> 0;
|
|
443
|
+
return t / 4294967296;
|
|
444
|
+
};
|
|
445
|
+
for (let i = 0; i < 12; i++) sfc32();
|
|
446
|
+
Math.random = sfc32;
|
|
447
|
+
}
|
|
393
448
|
if (!isMainThread && parentPort) {
|
|
449
|
+
const seed = workerData?.mathRandomSeed;
|
|
450
|
+
if (typeof seed === "string") seedMathRandom(seed);
|
|
394
451
|
const port = parentPort;
|
|
395
452
|
const post = (msg, transfer) => {
|
|
396
453
|
port.postMessage(msg, transfer ?? transferList(msg));
|
|
@@ -404,5 +461,6 @@ if (!isMainThread && parentPort) {
|
|
|
404
461
|
export {
|
|
405
462
|
createWorkerHost,
|
|
406
463
|
handleMessage,
|
|
464
|
+
seedMathRandom,
|
|
407
465
|
transferList
|
|
408
466
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/alex-irt/irtio.git",
|
|
9
|
+
"directory": "packages/runtime"
|
|
10
|
+
},
|
|
6
11
|
"publishConfig": {
|
|
7
12
|
"access": "public"
|
|
8
13
|
},
|
|
@@ -31,9 +36,9 @@
|
|
|
31
36
|
"@dimforge/rapier2d-compat": "0.20.0",
|
|
32
37
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
33
38
|
"matter-js": "0.20.0",
|
|
34
|
-
"@irtio/protocol": "0.
|
|
35
|
-
"@irtio/schema": "0.
|
|
36
|
-
"@irtio/server": "0.
|
|
39
|
+
"@irtio/protocol": "3.0.0",
|
|
40
|
+
"@irtio/schema": "3.0.0",
|
|
41
|
+
"@irtio/server": "3.0.0"
|
|
37
42
|
},
|
|
38
43
|
"devDependencies": {
|
|
39
44
|
"@types/matter-js": "0.20.2"
|