@irtio/runtime 1.0.0 → 3.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.
@@ -3,6 +3,80 @@ import * as _irtio_protocol from '@irtio/protocol';
3
3
  import { PlatformNotice, ErrorCodeName } from '@irtio/protocol';
4
4
  import { RoomDefinition, Room, LeaveReason, NpcConfig } from '@irtio/server';
5
5
 
6
+ /**
7
+ * M7 pf4: the per-room RPC trace ring.
8
+ *
9
+ * One entry per client→server RPC: who called, what they called, the two clocks the call was
10
+ * judged against (`ctx.tick` and `ctx.clientTick`), how it ended, and a truncated summary of what
11
+ * came back. It is a **separate** store from `EventRing`, on purpose. The event ring is always on
12
+ * and 100 entries wide, and it is the thing every deployed room pays for; making it rich enough to
13
+ * answer "why did this shot produce no tracer" would make every production room pay for a
14
+ * development question. So this ring is armed — `irtio dev --trace-rpc`, `SupervisorConfig.traceRpc`
15
+ * — and a room nobody armed never constructs one. `RoomCore.recordRpc` is a call into an
16
+ * `undefined`, which is the same cost `captureTimeline` pays when the recorder is unarmed.
17
+ *
18
+ * The stamp-lag table (ask b) rides here rather than in a second structure: the last traced call
19
+ * per client is kept in its own map, so a client whose entry has already been evicted from the ring
20
+ * still reports the lag its last call had. That is the whole reason the ask says "rides on whatever
21
+ * the RPC trace stores" — the server keeps no per-session stamp record otherwise.
22
+ */
23
+ /** Entries kept per room. Bounded because a dev session left running overnight is a real thing. */
24
+ declare const RPC_TRACE_SIZE = 200;
25
+ /**
26
+ * Longest result summary kept, in UTF-16 code units of JSON. A summary is a diagnostic, not a
27
+ * record: an RPC returning a 10 kB payload should cost the trace a line, not 10 kB × 200.
28
+ */
29
+ declare const RPC_TRACE_SUMMARY_MAX = 200;
30
+ /**
31
+ * How a call ended. `deny:<reason>` is `ctx.deny(reason)` — an application refusal, which is the
32
+ * distinction the whole ask exists for: a refusal is not an error, and a trace that spelled both
33
+ * `error` would answer the question with the answer that lost the evening.
34
+ */
35
+ type RpcOutcome = 'ok' | 'error' | `deny:${string}`;
36
+ interface RpcTraceEntry {
37
+ /** Monotonic within a room, so a reader can tell "no new calls" from "the ring rolled". */
38
+ readonly seq: number;
39
+ readonly clientId: string;
40
+ readonly rpc: string;
41
+ /** The server tick the call was applied at (`ctx.tick`). */
42
+ readonly tick: number;
43
+ /** The caller's own stamp (`ctx.clientTick`), absent when the client sent none. */
44
+ readonly clientTick?: number;
45
+ readonly outcome: RpcOutcome;
46
+ /** JSON-ish rendering of the reply, truncated at {@link RPC_TRACE_SUMMARY_MAX}. */
47
+ readonly result?: string;
48
+ }
49
+ /** One client's last traced call — what the inspector's stamp-lag column reads. */
50
+ interface StampLag {
51
+ readonly tick: number;
52
+ readonly clientTick: number | undefined;
53
+ /** `ctx.tick - ctx.clientTick`, or `undefined` when the call carried no stamp. */
54
+ readonly lag: number | undefined;
55
+ readonly rpc: string;
56
+ readonly seq: number;
57
+ }
58
+ /** Renders a reply for the trace: JSON, truncated, and never able to throw. */
59
+ declare function summarize(value: unknown): string | undefined;
60
+ declare class RpcTraceRing {
61
+ private readonly items;
62
+ private readonly lastByClient;
63
+ private seq;
64
+ push(entry: Omit<RpcTraceEntry, 'seq'>): RpcTraceEntry;
65
+ list(): readonly RpcTraceEntry[];
66
+ /** Client id → its last traced call. Survives the entry itself being evicted from the ring. */
67
+ stampLag(): ReadonlyMap<string, StampLag>;
68
+ /** A client that left takes its stamp row with it; the ring entries stay. */
69
+ forget(clientId: string): void;
70
+ /** JSON shape served by `/__irt/rpc-trace.json` and carried on `inspect`. */
71
+ dump(): RpcTraceDump;
72
+ }
73
+ interface RpcTraceDump {
74
+ readonly entries: readonly RpcTraceEntry[];
75
+ readonly stampLag: readonly ({
76
+ readonly clientId: string;
77
+ } & StampLag)[];
78
+ }
79
+
6
80
  /**
7
81
  * D41: the recorded authoritative timeline.
8
82
  *
@@ -429,6 +503,14 @@ interface RoomCoreOptions {
429
503
  * `testRoom({ profile: true })` are what turn it on.
430
504
  */
431
505
  readonly profile?: boolean;
506
+ /**
507
+ * M7 pf4: keep a bounded RPC trace ring for this room. Off by default and free when off — a
508
+ * room nobody armed constructs no ring and `recordRpc` is a call into an `undefined`, exactly
509
+ * as `captureTimeline` is when the D41 recorder is unarmed. `irtio dev --trace-rpc`,
510
+ * `SupervisorConfig.traceRpc` and `testRoom({ traceRpc: true })` are what turn it on; nothing
511
+ * in a deployed tenant does.
512
+ */
513
+ readonly traceRpc?: boolean;
432
514
  /**
433
515
  * D77 / O28: the write core's caps — encoded state bytes, records per collection, writes per
434
516
  * client per second. A host that sets none gets the record cap and nothing else, which is what
@@ -460,6 +542,16 @@ interface JoinOptions {
460
542
  readonly playerId?: string;
461
543
  /** D44: this join is a scripted NPC's loopback session. */
462
544
  readonly npc?: boolean;
545
+ /**
546
+ * The **verified** identity subject behind this join, or `undefined` when there is none.
547
+ *
548
+ * Deliberately not `playerId`. `playerId` always has a value — it falls back to the client id —
549
+ * and that fallback is exactly what a moderation decision must not be fooled by: a key join's
550
+ * `playerId` is a resume token's client id, which a returning stranger simply mints again. This
551
+ * is the narrower fact: present only when a credential was verified, absent for a key join, and
552
+ * therefore the only thing `room.ban` can key a cooldown on honestly.
553
+ */
554
+ readonly subject?: string;
463
555
  }
464
556
  interface JoinResult {
465
557
  readonly tick: number;
@@ -472,6 +564,29 @@ declare class RoomFullError extends Error {
472
564
  readonly name = "RoomFullError";
473
565
  constructor(maxClients: number);
474
566
  }
567
+ /**
568
+ * M8 lane 5: this join was refused before it had a seat.
569
+ *
570
+ * Thrown by `RoomCore.join` for all four of the room's own refusals — a subject inside a
571
+ * `room.ban` cooldown, an `onAdmit` that called `ctx.deny`, an `onAdmit` that never answered
572
+ * (`admission timed out`), and an `onAdmit` that threw (`admission check failed`) — and mapped to
573
+ * `E_ADMISSION` at the
574
+ * worker boundary, exactly where `RoomFullError` becomes `E_ROOM_FULL`. Its own class rather than
575
+ * a `DenyError` reaching the host, so the host can tell a refusal it should answer with a typed
576
+ * error from a handler that broke.
577
+ */
578
+ declare class AdmissionError extends Error {
579
+ readonly reason: string;
580
+ readonly name = "AdmissionError";
581
+ constructor(reason: string);
582
+ }
583
+ /**
584
+ * M7 pf3 (f): the smoothing on `RoomStats.physicsStepMsAvg`.
585
+ *
586
+ * 0.1 is roughly a ten-tick window: half a second at 20 Hz, which is short enough to show a pile
587
+ * of crates settling and long enough that one preempted step does not read as a regression.
588
+ */
589
+ declare const PHYSICS_STEP_EMA_ALPHA = 0.1;
475
590
  interface RoomStats {
476
591
  ticks: number;
477
592
  lastTickMs: number;
@@ -488,6 +603,22 @@ interface RoomStats {
488
603
  gridBuildMs: number;
489
604
  gridQueryMs: number;
490
605
  aoiEncodeMs: number;
606
+ /**
607
+ * Wall milliseconds the last world step took, measured around the engine's own `step()` and
608
+ * nothing else: not `reconcile`, not `sync`, not the room's `tick()` handler. `0` in a room
609
+ * with no physics, and `0` until the first step.
610
+ *
611
+ * Deliberately narrow. `lastTickMs` already covers the whole tick, and the question this
612
+ * answers — "is the engine the thing that is slow?" — is the one a room cannot answer by
613
+ * subtracting numbers it does not have.
614
+ */
615
+ physicsStepMs: number;
616
+ /**
617
+ * An exponential moving average of {@link physicsStepMs}, alpha {@link PHYSICS_STEP_EMA_ALPHA}.
618
+ * A single step's timing is mostly noise on a shared machine; this is the number to compare
619
+ * against a budget or to plot.
620
+ */
621
+ physicsStepMsAvg: number;
491
622
  visibleIdsTotal: number;
492
623
  membershipEnters: number;
493
624
  membershipLeaves: number;
@@ -530,6 +661,14 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
530
661
  receive(clientId: string, frame: Uint8Array): void;
531
662
  /** Full state + tick + rng for hibernation. Runs `onSleep`. Clears timers. */
532
663
  serialize(): Uint8Array;
664
+ /**
665
+ * The earliest pending room-timer deadline the last `serialize()` cleared, on `host.now()`'s
666
+ * clock, or `undefined` if there was none. The worker translates it to wall clock and hands it
667
+ * to the supervisor, which wakes the room at it — a `room.setTimeout` loses its callback to a
668
+ * hibernation but no longer loses the room's chance to act on the deadline. `undefined` until
669
+ * the first `serialize()`, and never touched by `snapshot()`.
670
+ */
671
+ readonly lastSerializeTimerAt: number | undefined;
533
672
  /**
534
673
  * D24: the same bytes `serialize()` produces, with none of its side effects — the room carries
535
674
  * on exactly as it was. This is what a save generation is written from; `serialize()` is what
@@ -553,6 +692,12 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
553
692
  recording(): TimelineDump | undefined;
554
693
  /** D41: capture one tick. Called by the loop; a no-op while the recorder is unarmed. */
555
694
  captureTimeline(): void;
695
+ /**
696
+ * M7 pf4: the RPC trace so far, or `undefined` when this room was not started with
697
+ * `traceRpc: true`. Like `profile()`, `undefined` and "empty" are different readings: a room
698
+ * nobody armed and a room nobody called must not look the same.
699
+ */
700
+ rpcTrace(): RpcTraceDump | undefined;
556
701
  /**
557
702
  * Week 12: settles the promise `hostCall(reqId, …)` returned. The host calls this from its own
558
703
  * turn; the runtime runs the continuation as a discrete event and flushes afterwards, so state
@@ -587,4 +732,4 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
587
732
  askCount(): number;
588
733
  }
589
734
 
590
- export { DEFAULT_RECORDS_PER_COLLECTION as D, EVENT_RING_SIZE as E, HOST_CALL_TIMEOUT_MS as H, type JoinOptions as J, type LogLevel as L, RELAY_RECORDS_PER_COLLECTION as R, type TimelineDump as T, type WriteCaps as W, DEFAULT_TIMELINE_MAX_RECORDS as a, DEFAULT_TIMELINE_MAX_TICKS as b, type HostCall as c, type HostCallResult as d, type JoinResult as e, RELAY_STATE_BYTES as f, RELAY_WRITES_PER_SECOND as g, RELAY_WRITE_CAPS as h, type RoomCoreApi as i, type RoomCoreOptions as j, type RoomEvent as k, type RoomEventKind as l, RoomFullError as m, type RoomHost as n, type RoomInspection as o, type RoomStats as p, type TimelineFrame as q, TimelineRecorder as r, type TimelineRecorderOptions as s, inspectState as t };
735
+ export { AdmissionError as A, summarize as B, DEFAULT_RECORDS_PER_COLLECTION as D, EVENT_RING_SIZE as E, HOST_CALL_TIMEOUT_MS as H, type JoinOptions as J, type LogLevel as L, PHYSICS_STEP_EMA_ALPHA as P, RELAY_RECORDS_PER_COLLECTION as R, type StampLag as S, type TimelineDump as T, type WriteCaps as W, DEFAULT_TIMELINE_MAX_RECORDS as a, DEFAULT_TIMELINE_MAX_TICKS as b, type HostCall as c, type HostCallResult as d, type JoinResult as e, RELAY_STATE_BYTES as f, RELAY_WRITES_PER_SECOND as g, RELAY_WRITE_CAPS as h, RPC_TRACE_SIZE as i, RPC_TRACE_SUMMARY_MAX as j, type RoomCoreApi as k, type RoomCoreOptions as l, type RoomEvent as m, type RoomEventKind as n, RoomFullError as o, type RoomHost as p, type RoomInspection as q, type RoomStats as r, type RpcOutcome as s, type RpcTraceDump as t, type RpcTraceEntry as u, RpcTraceRing as v, type TimelineFrame as w, TimelineRecorder as x, type TimelineRecorderOptions as y, inspectState as z };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { 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, R as RELAY_RECORDS_PER_COLLECTION, f as RELAY_STATE_BYTES, g as RELAY_WRITES_PER_SECOND, h as RELAY_WRITE_CAPS, i as RoomCoreApi, j as RoomCoreOptions, k as RoomEvent, l as RoomEventKind, m as RoomFullError, n as RoomHost, o as RoomInspection, p as RoomStats, T as TimelineDump, q as TimelineFrame, r as TimelineRecorder, s as TimelineRecorderOptions, W as WriteCaps, t as inspectState } from './contract-UEKTud1Z.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-uhj3b-8E.js';
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-BoCNAPZT.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-CGbiPxi3.js';
3
3
  import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
4
4
  import '@irtio/protocol';
5
5
  import '@irtio/server';
@@ -28,7 +28,10 @@ declare const RPC_TIMEOUT_MS = 5000;
28
28
  * adds and removes, so a client never learns such a record exists.
29
29
  */
30
30
 
31
- /** Is `c` visible to `role`? */
31
+ /**
32
+ * Is `c` visible to `role`? A `visibility: 'server'` collection is visible to none — the same
33
+ * answer `visibility: 'role'` with an empty `roles` list has always given, said on purpose.
34
+ */
32
35
  declare function isVisible(c: CollectionDesc, role: string): boolean;
33
36
  /** A `SnapshotOptions.collections` predicate for one role. */
34
37
  declare function visibleTo(role: string): (c: CollectionDesc) => boolean;
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@ import {
2
2
  fromMigrationState,
3
3
  migrateSnapshot,
4
4
  toMigrationState
5
- } from "./chunk-YWDRE77H.js";
5
+ } from "./chunk-3BCTP4YS.js";
6
6
  import {
7
+ AdmissionError,
7
8
  CRASH_AFTER_THROWS,
8
9
  DEFAULT_RECORDS_PER_COLLECTION,
9
10
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -12,14 +13,18 @@ import {
12
13
  HOST_CALL_TIMEOUT_MS,
13
14
  MAX_CATCHUP,
14
15
  Mulberry32,
16
+ PHYSICS_STEP_EMA_ALPHA,
15
17
  READABLE_SNAPSHOT_VERSIONS,
16
18
  RELAY_RECORDS_PER_COLLECTION,
17
19
  RELAY_STATE_BYTES,
18
20
  RELAY_WRITES_PER_SECOND,
19
21
  RELAY_WRITE_CAPS,
20
22
  RPC_TIMEOUT_MS,
23
+ RPC_TRACE_SIZE,
24
+ RPC_TRACE_SUMMARY_MAX,
21
25
  RoomCore,
22
26
  RoomFullError,
27
+ RpcTraceRing,
23
28
  SNAPSHOT_FORMAT_VERSION,
24
29
  TimelineRecorder,
25
30
  catchUpDirty,
@@ -48,12 +53,14 @@ import {
48
53
  resetMatterForTests,
49
54
  resetPhysicsForTests,
50
55
  resetRapier2dForTests,
56
+ summarize,
51
57
  viewKeyFor,
52
58
  visibleNames,
53
59
  visibleTo,
54
60
  writeHibernationBlob
55
- } from "./chunk-LT46QFLW.js";
61
+ } from "./chunk-NPRIYYNM.js";
56
62
  export {
63
+ AdmissionError,
57
64
  CRASH_AFTER_THROWS,
58
65
  DEFAULT_RECORDS_PER_COLLECTION,
59
66
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -62,14 +69,18 @@ export {
62
69
  HOST_CALL_TIMEOUT_MS,
63
70
  MAX_CATCHUP,
64
71
  Mulberry32,
72
+ PHYSICS_STEP_EMA_ALPHA,
65
73
  READABLE_SNAPSHOT_VERSIONS,
66
74
  RELAY_RECORDS_PER_COLLECTION,
67
75
  RELAY_STATE_BYTES,
68
76
  RELAY_WRITES_PER_SECOND,
69
77
  RELAY_WRITE_CAPS,
70
78
  RPC_TIMEOUT_MS,
79
+ RPC_TRACE_SIZE,
80
+ RPC_TRACE_SUMMARY_MAX,
71
81
  RoomCore,
72
82
  RoomFullError,
83
+ RpcTraceRing,
73
84
  SNAPSHOT_FORMAT_VERSION,
74
85
  TimelineRecorder,
75
86
  catchUpDirty,
@@ -100,6 +111,7 @@ export {
100
111
  resetMatterForTests,
101
112
  resetPhysicsForTests,
102
113
  resetRapier2dForTests,
114
+ summarize as summarizeRpcResult,
103
115
  toMigrationState,
104
116
  viewKeyFor,
105
117
  visibleNames,