@irtio/runtime 0.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.
@@ -0,0 +1,120 @@
1
+ import { AnySchema, PlainState, State, Tracked } from '@irtio/schema';
2
+ import { ErrorCodeName } from '@irtio/protocol';
3
+ import { RoomDefinition, Room, LeaveReason } from '@irtio/server';
4
+
5
+ /**
6
+ * Live inspection for the dev page (`irtio dev`) and the supervisor admin API: a JSON view of
7
+ * the room's state plus a ring of recent events. Always on; recording is O(1) per event.
8
+ */
9
+
10
+ type RoomEventKind = 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'error';
11
+ interface RoomEvent {
12
+ readonly tick: number;
13
+ readonly kind: RoomEventKind;
14
+ readonly clientId?: string;
15
+ readonly detail?: string;
16
+ }
17
+ declare const EVENT_RING_SIZE = 100;
18
+ /** JSON-able view of a plain state: entities as `{id: {owner, value}}`, singletons as values. */
19
+ declare function inspectState(schema: AnySchema, plain: PlainState): Record<string, unknown>;
20
+ interface RoomInspection {
21
+ readonly tick: number;
22
+ readonly state: Record<string, unknown>;
23
+ readonly recent: readonly RoomEvent[];
24
+ }
25
+
26
+ type LogLevel = 'info' | 'warn' | 'error';
27
+ /** Everything the room needs from the outside world. No timers, sockets, clocks, or console inside. */
28
+ interface RoomHost {
29
+ /** Monotonic ms. Fake in the harness. */
30
+ now(): number;
31
+ setTimeout(fn: () => void, ms: number): unknown;
32
+ clearTimeout(handle: unknown): void;
33
+ /** Deliver one fully framed protocol frame (type byte + payload) to a client. */
34
+ send(clientId: string, frame: Uint8Array): void;
35
+ /** The room wants this client gone (`room.kick`, protocol violation). Host closes the socket. */
36
+ kick(clientId: string, code: ErrorCodeName, reason?: string): void;
37
+ /** `room.close()`: host disconnects everyone and tears the room down. */
38
+ close(reason?: string): void;
39
+ /** Event mode: idle or `room.sleep()` — host should `serialize()` then `stop()`. */
40
+ sleep(): void;
41
+ log(level: LogLevel, args: unknown[]): void;
42
+ /** N consecutive tick throws; the runtime does not recover itself. */
43
+ crashed(reason: string): void;
44
+ }
45
+ interface RoomCoreOptions {
46
+ readonly roomId: string;
47
+ /** Seed for `room.random()`; default 1. */
48
+ readonly seed?: number;
49
+ /** Base URL for `room.link` (`<publicUrl>?room=<id>`); default `http://localhost/`. */
50
+ readonly publicUrl?: string;
51
+ /** Bytes from a previous `serialize()`; skips `onCreate`, runs `onWake`. */
52
+ readonly restoreFrom?: Uint8Array;
53
+ }
54
+ interface JoinOptions {
55
+ /** Requested role; unknown/missing → `roles[0]` (logged) or `''` when the schema has no roles. */
56
+ readonly role?: string;
57
+ readonly name?: string;
58
+ /** A resumed session: presence record kept, `ctx.reconnecting = true`. */
59
+ readonly reconnecting?: boolean;
60
+ }
61
+ interface JoinResult {
62
+ readonly tick: number;
63
+ readonly role: string;
64
+ /** Snapshot bytes for this client's view (extended schema). The host builds `WELCOME`. */
65
+ readonly snapshot: Uint8Array;
66
+ }
67
+ declare class RoomFullError extends Error {
68
+ readonly maxClients: number;
69
+ readonly name = "RoomFullError";
70
+ constructor(maxClients: number);
71
+ }
72
+ interface RoomStats {
73
+ ticks: number;
74
+ lastTickMs: number;
75
+ maxTickMs: number;
76
+ overruns: number;
77
+ framesIn: number;
78
+ framesOut: number;
79
+ bytesOut: number;
80
+ /** Per connected client. */
81
+ bytesOutByClient: Map<string, number>;
82
+ handlerErrors: number;
83
+ encodesLastFlush: number;
84
+ corrections: number;
85
+ }
86
+ /** The surface the worker host and the harness drive. Implemented by `RoomCore`. */
87
+ interface RoomCoreApi<S extends AnySchema = AnySchema> {
88
+ readonly definition: RoomDefinition<S>;
89
+ /** The builder's schema. */
90
+ readonly schema: S;
91
+ /** `withBuiltins(schema)`: what the codec and every frame use. */
92
+ readonly ext: AnySchema;
93
+ readonly roomId: string;
94
+ readonly tick: number;
95
+ /** Tracked authority (proxy tree incl. the built-in `clients` collection). */
96
+ readonly state: State<S>;
97
+ readonly tracked: Tracked<AnySchema>;
98
+ /** The `room` API object handed to handlers. */
99
+ readonly room: Room<S>;
100
+ readonly stats: RoomStats;
101
+ readonly mode: 'tick' | 'event';
102
+ /** Begin ticking (tick mode) / idle timer (event mode). Idempotent. */
103
+ start(): void;
104
+ /** Stop timers; no more frames are sent. */
105
+ stop(): void;
106
+ /** Throws `RoomFullError`. Runs `onJoin`, then snapshots the client's view. */
107
+ join(clientId: string, options?: JoinOptions): JoinResult;
108
+ /** Runs `onLeave`, removes presence, rejects pending RPCs for the client. */
109
+ leave(clientId: string, reason: LeaveReason): void;
110
+ /** Grace window opened by the host: presence `connected = false`; frames are no longer sent. */
111
+ markDisconnected(clientId: string): void;
112
+ /** One inbound protocol frame (`WRITE` | `CALL` | `REPLY` | `MSG`). Anything else → host.kick(E_BAD_FRAME). */
113
+ receive(clientId: string, frame: Uint8Array): void;
114
+ /** Full state + tick + rng for hibernation. Runs `onSleep`. Clears timers. */
115
+ serialize(): Uint8Array;
116
+ /** Live JSON view of the room (dev page / supervisor admin API). */
117
+ inspect(): RoomInspection;
118
+ }
119
+
120
+ export { EVENT_RING_SIZE as E, type JoinOptions as J, type LogLevel as L, type RoomCoreApi as R, type JoinResult as a, type RoomCoreOptions as b, type RoomEvent as c, type RoomEventKind as d, RoomFullError as e, type RoomHost as f, type RoomInspection as g, type RoomStats as h, inspectState as i };
@@ -0,0 +1,161 @@
1
+ export { E as EVENT_RING_SIZE, J as JoinOptions, a as JoinResult, L as LogLevel, R as RoomCoreApi, b as RoomCoreOptions, c as RoomEvent, d as RoomEventKind, e as RoomFullError, f as RoomHost, g as RoomInspection, h as RoomStats, i as inspectState } from './contract-BhD88PGb.js';
2
+ export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as Mulberry32, R as RoomCore } from './room-CBsSCueH.js';
3
+ import { AnySchema, PlainState, DirtySet, CollectionDesc } from '@irtio/schema';
4
+ import '@irtio/protocol';
5
+ import '@irtio/server';
6
+
7
+ /**
8
+ * RPCs in both directions.
9
+ *
10
+ * client → server: `CALL` → `rpcByIdOf` → param decode + `validateValue` → the room's `rpc`
11
+ * implementation (or the built-in `requestOwnership`) → `REPLY`. A throw becomes an error reply;
12
+ * nothing escapes into the loop.
13
+ *
14
+ * server → client: `room.call(clientId).<name>(params)` returns a Promise resolved by the
15
+ * client's `REPLY`, rejected on a 5 s host-clock timeout or on disconnect.
16
+ * `room.broadcast.<name>(params)` fans one `CALL` out with no pending entry.
17
+ */
18
+
19
+ /** Default server→client RPC timeout on the host clock. */
20
+ declare const RPC_TIMEOUT_MS = 5000;
21
+
22
+ /**
23
+ * Views: which collections a role sees, how clients bucket into distinct views (so one flush
24
+ * encodes one `DELTA` per view, not per client), the per-view snapshot taken at `join`, and
25
+ * the catch-up delta `room.setRole` sends.
26
+ *
27
+ * A collection with `visibility: 'role'` is visible only to the roles it names — including its
28
+ * adds and removes, so a client never learns such a record exists.
29
+ */
30
+
31
+ /** Is `c` visible to `role`? */
32
+ declare function isVisible(c: CollectionDesc, role: string): boolean;
33
+ /** A `SnapshotOptions.collections` predicate for one role. */
34
+ declare function visibleTo(role: string): (c: CollectionDesc) => boolean;
35
+ /** Names of the collections `role` sees (memoised per schema + role). */
36
+ declare function visibleNames(ext: AnySchema, role: string): ReadonlySet<string>;
37
+ /**
38
+ * The view bucket for a role: `'all'` when the role is not named by any role-scoped collection
39
+ * (every such role sees exactly the same set), otherwise the role itself.
40
+ */
41
+ declare function viewKeyFor(ext: AnySchema, role: string): string;
42
+ /** The `join` snapshot: full state restricted to the client's view. */
43
+ declare function encodeViewSnapshot(ext: AnySchema, plain: PlainState, role: string, tick: number): Uint8Array;
44
+ /** One view's `DELTA` payload, or `null` when nothing visible to `role` changed. */
45
+ declare function encodeViewDelta(ext: AnySchema, plain: PlainState, dirty: DirtySet, role: string, tick: number): Uint8Array | null;
46
+ /**
47
+ * `room.setRole` catch-up: `add` ops for every record of a collection that just became visible
48
+ * and `remove` ops for every record of one that stopped being visible. A singleton that becomes
49
+ * visible is sent as a whole-record update; one that stops being visible cannot be un-sent (the
50
+ * format has no singleton remove), so it is left alone — the client keeps stale values it can no
51
+ * longer see updated. Returns an empty set when the two roles have the same view.
52
+ */
53
+ declare function catchUpDirty(ext: AnySchema, plain: PlainState, fromRole: string, toRole: string): DirtySet;
54
+
55
+ /**
56
+ * The hibernation blob envelope: a small fixed header the runtime writes around the codec
57
+ * snapshot, so a woken room gets its rng, tick and mode back.
58
+ *
59
+ * u8 format version
60
+ * u32 seed
61
+ * u32 rng state
62
+ * u32 tick
63
+ * u8 mode (1 = event, 0 = tick)
64
+ * ... encodeSnapshot(withBuiltins(schema), state)
65
+ *
66
+ * Note: the *deployment* version is deliberately **not** in here. It lives in the store
67
+ * key (`<project>/<room>@v<deployment>`), which keeps the blob format frozen — an old snapshot
68
+ * stays valid across deployments — and lets a wake path discover the version by listing keys
69
+ * instead of parsing bytes it may not yet know how to read.
70
+ */
71
+ declare const SNAPSHOT_FORMAT_VERSION = 1;
72
+ type SnapshotMode = 'tick' | 'event';
73
+ interface SnapshotHeader {
74
+ readonly seed: number;
75
+ readonly rngState: number;
76
+ readonly tick: number;
77
+ readonly mode: SnapshotMode;
78
+ }
79
+ interface ParsedSnapshot extends SnapshotHeader {
80
+ /** The codec snapshot, encoded under `withBuiltins(schema)`. */
81
+ readonly snapshot: Uint8Array;
82
+ }
83
+ declare function parseHibernationBlob(bytes: Uint8Array): ParsedSnapshot;
84
+ declare function writeHibernationBlob(header: SnapshotHeader, snapshot: Uint8Array): Uint8Array;
85
+
86
+ /**
87
+ * Wake-time schema migration.
88
+ *
89
+ * A room hibernated under deployment v1 and woken under v3 has bytes on disk that only v1's
90
+ * schema can decode. `migrateSnapshot` decodes them with the *stored* canonical schema of the
91
+ * deployment they were written under, hands the plain state to each deployment's `up` transform
92
+ * in order, and re-encodes under the target schema. The supervisor re-snapshots immediately
93
+ * afterwards, so the chain runs exactly once per room ("migrate once").
94
+ *
95
+ * The state a migration sees is deliberately plain JSON-ish, not the tracked proxy tree:
96
+ *
97
+ * { players: { 'c1': { owner: 'c1', value: { x: 1, y: 2 } } }, // entity
98
+ * match: { phase: 'lobby', round: 0 } } // singleton
99
+ *
100
+ * Entities are `{ id: { owner, value } }` maps; singletons are the record itself. The built-in
101
+ * `clients` presence collection is removed before `up` runs and comes back empty afterwards —
102
+ * presence is rebuilt from the sockets that rejoin, so a migration never has to think about it.
103
+ */
104
+
105
+ interface MigrationRecord {
106
+ owner: string;
107
+ value: Record<string, unknown>;
108
+ }
109
+ /** An entity collection as a migration sees it: id → `{ owner, value }`. */
110
+ type MigrationEntity = Record<string, MigrationRecord>;
111
+ /** A whole room's state: entity maps and singleton records, keyed by collection name. */
112
+ type MigrationState = Record<string, MigrationEntity | Record<string, unknown>>;
113
+ /** Handed to `up` as its second argument. Small on purpose, to keep migrations honest. */
114
+ interface MigrationHelpers {
115
+ /** The deployment version this migration produces. */
116
+ readonly version: number;
117
+ /** The deployment version its input was written under. */
118
+ readonly fromVersion: number;
119
+ /** The room being migrated. */
120
+ readonly roomId: string;
121
+ /** Goes to the room's log, prefixed with the version. */
122
+ log(...args: unknown[]): void;
123
+ }
124
+ interface MigrationModule {
125
+ up(state: MigrationState, s: MigrationHelpers): MigrationState | void;
126
+ /** Scaffolded by `irtio migrate create` and stored; unused until rollback ships. */
127
+ down?(state: MigrationState, s: MigrationHelpers): MigrationState | void;
128
+ }
129
+ /**
130
+ * One deployment in the chain the tenant was handed. `migration` is absent for versions whose
131
+ * schema change was purely additive — the codec fills new fields from their defaults, so there
132
+ * is nothing to transform.
133
+ */
134
+ interface MigrationStep {
135
+ readonly version: number;
136
+ /** `schema.canonical` as stored on the deployment record. */
137
+ readonly schemaJson: string;
138
+ readonly migration?: MigrationModule;
139
+ }
140
+ interface MigrateOptions {
141
+ readonly roomId: string;
142
+ log?(level: 'info' | 'warn' | 'error', args: unknown[]): void;
143
+ }
144
+ interface MigrationOutcome {
145
+ /** A hibernation blob encoded under the last step's schema. */
146
+ readonly bytes: Uint8Array;
147
+ readonly fromVersion: number;
148
+ readonly toVersion: number;
149
+ /** Versions whose `up` actually ran. */
150
+ readonly applied: readonly number[];
151
+ readonly ms: number;
152
+ }
153
+ /**
154
+ * Runs the chain. `chain` must be sorted ascending and start with the deployment the snapshot
155
+ * was written under (its `up` is *not* run — it is only there to supply the decoding schema).
156
+ */
157
+ declare function migrateSnapshot(blob: Uint8Array, chain: readonly MigrationStep[], options: MigrateOptions): MigrationOutcome;
158
+ declare function toMigrationState(schema: AnySchema, plain: PlainState): MigrationState;
159
+ declare function fromMigrationState(schema: AnySchema, state: MigrationState, version: number): PlainState;
160
+
161
+ export { type MigrateOptions, type MigrationEntity, type MigrationHelpers, type MigrationModule, type MigrationOutcome, type MigrationRecord, type MigrationState, type MigrationStep, type ParsedSnapshot, RPC_TIMEOUT_MS, SNAPSHOT_FORMAT_VERSION, type SnapshotHeader, type SnapshotMode, catchUpDirty, encodeViewDelta, encodeViewSnapshot, fromMigrationState, isVisible, migrateSnapshot, parseHibernationBlob, toMigrationState, viewKeyFor, visibleNames, visibleTo, writeHibernationBlob };
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import {
2
+ fromMigrationState,
3
+ migrateSnapshot,
4
+ toMigrationState
5
+ } from "./chunk-3DQHT4CM.js";
6
+ import {
7
+ CRASH_AFTER_THROWS,
8
+ EVENT_RING_SIZE,
9
+ MAX_CATCHUP,
10
+ Mulberry32,
11
+ RPC_TIMEOUT_MS,
12
+ RoomCore,
13
+ RoomFullError,
14
+ SNAPSHOT_FORMAT_VERSION,
15
+ catchUpDirty,
16
+ encodeViewDelta,
17
+ encodeViewSnapshot,
18
+ inspectState,
19
+ isVisible,
20
+ parseHibernationBlob,
21
+ viewKeyFor,
22
+ visibleNames,
23
+ visibleTo,
24
+ writeHibernationBlob
25
+ } from "./chunk-X5S364FY.js";
26
+ export {
27
+ CRASH_AFTER_THROWS,
28
+ EVENT_RING_SIZE,
29
+ MAX_CATCHUP,
30
+ Mulberry32,
31
+ RPC_TIMEOUT_MS,
32
+ RoomCore,
33
+ RoomFullError,
34
+ SNAPSHOT_FORMAT_VERSION,
35
+ catchUpDirty,
36
+ encodeViewDelta,
37
+ encodeViewSnapshot,
38
+ fromMigrationState,
39
+ inspectState,
40
+ isVisible,
41
+ migrateSnapshot,
42
+ parseHibernationBlob,
43
+ toMigrationState,
44
+ viewKeyFor,
45
+ visibleNames,
46
+ visibleTo,
47
+ writeHibernationBlob
48
+ };
@@ -0,0 +1,246 @@
1
+ import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
2
+ import { RoomDefinition, RoomMode, Room, Ctx, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
3
+ import { f as RoomHost, h as RoomStats, L as LogLevel, R as RoomCoreApi, b as RoomCoreOptions, d as RoomEventKind, g as RoomInspection, J as JoinOptions, a as JoinResult } from './contract-BhD88PGb.js';
4
+
5
+ /**
6
+ * `room.random()` — mulberry32 over a u32 seed. Deterministic, tiny, and serializable: the
7
+ * generator's whole state is one u32, so `serialize()`/`restore()` round-trip it exactly.
8
+ */
9
+ declare class Mulberry32 {
10
+ /** Current internal state (u32). Survives hibernation. */
11
+ state: number;
12
+ constructor(seed: number);
13
+ /** Next float in `[0, 1)`. */
14
+ next(): number;
15
+ }
16
+
17
+ /**
18
+ * The internal seam between `RoomCore` (core/room.ts) and the modules it delegates to
19
+ * (`loop`, `views`, `writes`, `rpc`, `messages`, `room-api`). Nothing here is public API —
20
+ * `src/contract.ts` is. `RoomCore` implements `RoomInternals` structurally, so the modules
21
+ * take it as a parameter and never import `room.ts` (no cycles).
22
+ */
23
+
24
+ type AnyRecord = Record<string, unknown>;
25
+ /** `${collection}\0${id}` → leaf-path key → the value the runtime wrote for that leaf. */
26
+ type AcceptedWrites = Map<string, Map<string, unknown>>;
27
+ /** One joined client. Presence is the wire truth; this is the runtime's bookkeeping. */
28
+ interface ClientEntry {
29
+ readonly clientId: string;
30
+ role: string;
31
+ name: string;
32
+ connected: boolean;
33
+ /** Pending `CORRECT` mask, encoded from current state at the next flush. */
34
+ correction: DirtySet | undefined;
35
+ /** Leaves this client's own accepted `WRITE`s produced during the current flush window. */
36
+ readonly accepted: AcceptedWrites;
37
+ /**
38
+ * The tick stamped on the last `WRITE` this client sent (the client-local write counter in the
39
+ * delta header — week 8, D19), `0` before its first write. Every `CORRECT` sent to this client
40
+ * carries it as `clientTick`: "your writes through this tick are reflected in these values".
41
+ * An unprompted correction (a server-wins overwrite with no triggering write) echoes it
42
+ * unchanged — for a client that never wrote, that is `0`.
43
+ */
44
+ lastClientTick: number;
45
+ /**
46
+ * Set at `join`: this client's own record(s) — its presence row and whatever entities its own
47
+ * `onJoin` added, owned by it — as they stood right after join, per collection. Consumed once
48
+ * by the very next `flush()`, which strips exactly the ids whose value (and owner) still match
49
+ * this capture from this client's delta only — an id mutated again before that flush (e.g. a
50
+ * same-window `room.setRole`) is left alone, since the join snapshot never saw that value.
51
+ * Every other client's delta is unaffected.
52
+ */
53
+ pendingJoinAdds: Map<string, ReadonlyMap<string, JoinAddCapture>> | undefined;
54
+ }
55
+ /** What `join` captured for one of a client's own just-added records, for `flush`'s comparison. */
56
+ interface JoinAddCapture {
57
+ readonly value: unknown;
58
+ readonly owner: string | undefined;
59
+ }
60
+ type GuardResult<T> = {
61
+ readonly ok: true;
62
+ readonly value: T;
63
+ } | {
64
+ readonly ok: false;
65
+ };
66
+ /** One queued inbound frame (tick mode). */
67
+ interface QueuedFrame {
68
+ readonly clientId: string;
69
+ readonly type: number;
70
+ readonly payload: Uint8Array;
71
+ }
72
+ interface LoopApi {
73
+ start(): void;
74
+ stop(): void;
75
+ /** Event mode: an inbound frame or a join re-arms the idle timer. */
76
+ noteActivity(): void;
77
+ /** `room.setTimeout` / `room.setInterval`. Returns a numeric handle. */
78
+ setTimer(ms: number, fn: () => void, repeat: boolean): number;
79
+ clearTimer(handle: number): void;
80
+ /** Drops every room timer (hibernation, stop). */
81
+ clearAllTimers(): void;
82
+ /** Internal one-shot on the host clock (RPC timeouts in event mode). */
83
+ after(ms: number, fn: () => void): void;
84
+ }
85
+ /** What the core modules are allowed to see of `RoomCore`. */
86
+ interface RoomInternals {
87
+ readonly definition: RoomDefinition;
88
+ /** `withBuiltins(definition.schema)` — what every frame and the codec use. */
89
+ readonly ext: AnySchema;
90
+ readonly host: RoomHost;
91
+ readonly roomId: string;
92
+ readonly mode: RoomMode;
93
+ /** The plain state the tracked proxies wrap (what the codec reads). */
94
+ readonly plain: PlainState;
95
+ readonly tracked: Tracked<AnySchema>;
96
+ /** The tracked proxy tree, loosely typed for internal use. */
97
+ readonly anyState: AnyRecord;
98
+ readonly room: Room;
99
+ readonly rng: Mulberry32;
100
+ readonly stats: RoomStats;
101
+ /** Joined clients in join order. */
102
+ readonly clients: Map<string, ClientEntry>;
103
+ readonly loop: LoopApi;
104
+ tick: number;
105
+ stopped: boolean;
106
+ /** Runs a room handler; a throw is logged and counted, never rethrown. */
107
+ guard<T>(name: string, fn: () => T): T | undefined;
108
+ recordEvent(kind: 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'error', clientId?: string, detail?: string): void;
109
+ /** `guard` that also reports whether the handler threw (the tick loop needs this). */
110
+ tryRun<T>(name: string, fn: () => T): GuardResult<T>;
111
+ log(level: LogLevel, ...args: unknown[]): void;
112
+ /** Sends one framed protocol frame to a connected client and counts it. */
113
+ send(clientId: string, frame: Uint8Array): void;
114
+ ctxFor(clientId: string, reconnecting?: boolean): Ctx;
115
+ /** The client's pending `CORRECT` dirty set, created on demand. */
116
+ correctionFor(clientId: string): DirtySet | undefined;
117
+ /** Flushes the tracked dirty set: corrections first, then one `DELTA` per distinct view. */
118
+ flush(): void;
119
+ /** Invalidates the cached `room.clients` array. */
120
+ invalidateClients(): void;
121
+ }
122
+
123
+ /**
124
+ * Scheduling: the tick-mode fixed-step loop with catch-up, event-mode idle detection, and
125
+ * `room.setTimeout` / `room.setInterval`.
126
+ *
127
+ * Tick mode: one host timeout re-armed every `1000 / tickRate` ms. Each wake accumulates real
128
+ * elapsed time and runs up to `MAX_CATCHUP` ticks; a bigger backlog is dropped and counted as an
129
+ * overrun (logged once per burst). Room timers are tick-granular: `setTimeout(ms)` fires on the
130
+ * first tick at or after `now + ms`.
131
+ *
132
+ * Event mode: no tick timer. Room timers fire on the host clock, each firing advancing the tick
133
+ * and flushing. After `idleMs` with no inbound frames and no connected clients the host is asked
134
+ * to hibernate exactly once (re-armed by the next frame or join).
135
+ */
136
+
137
+ /** Most ticks one wake may run before the backlog is dropped. */
138
+ declare const MAX_CATCHUP = 5;
139
+ /** Consecutive `tick()` throws before the host is told the room crashed. */
140
+ declare const CRASH_AFTER_THROWS = 3;
141
+ declare class Loop implements LoopApi {
142
+ private readonly core;
143
+ readonly inbound: QueuedFrame[];
144
+ private readonly timers;
145
+ private readonly internal;
146
+ private nextTimerId;
147
+ private tickHandle;
148
+ private idleHandle;
149
+ private running;
150
+ private lastWake;
151
+ private accumulator;
152
+ private overrunLogged;
153
+ private consecutiveThrows;
154
+ private lastActivity;
155
+ private slept;
156
+ constructor(core: RoomInternals);
157
+ get intervalMs(): number;
158
+ start(): void;
159
+ stop(): void;
160
+ enqueue(frame: QueuedFrame): void;
161
+ dropFramesFor(clientId: string): void;
162
+ private drainInbound;
163
+ /** Applies one queued/immediate frame. Returns `false` on a malformed payload. */
164
+ applyFrame(f: QueuedFrame): boolean;
165
+ private scheduleTick;
166
+ private onWake;
167
+ /** One tick: inbound → room timers → `tick(state, dt, room)` → flush. */
168
+ runTick(): void;
169
+ private fireDueTimers;
170
+ /** Applies one frame as its own event: tick++, apply, flush. */
171
+ applyEvent(f: QueuedFrame): boolean;
172
+ noteActivity(): void;
173
+ private armIdle;
174
+ private onIdleCheck;
175
+ setTimer(ms: number, fn: () => void, repeat: boolean): number;
176
+ private armHostTimer;
177
+ clearTimer(handle: number): void;
178
+ clearAllTimers(): void;
179
+ after(ms: number, fn: () => void): void;
180
+ }
181
+
182
+ /**
183
+ * `RoomCore` — the host-agnostic room runtime. It owns the extended schema, the
184
+ * tracked authority state, presence, the loop, and the frame dispatch; everything outside comes
185
+ * through `RoomHost` (`src/contract.ts`). The worker host and the test harness are adapters over
186
+ * exactly this surface and produce byte-identical frames.
187
+ */
188
+
189
+ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S> {
190
+ readonly definition: RoomDefinition<S>;
191
+ readonly ext: AnySchema;
192
+ readonly host: RoomHost;
193
+ readonly roomId: string;
194
+ readonly mode: RoomMode;
195
+ readonly plain: PlainState;
196
+ readonly tracked: Tracked<AnySchema>;
197
+ readonly anyState: AnyRecord;
198
+ readonly rng: Mulberry32;
199
+ readonly clients: Map<string, ClientEntry>;
200
+ readonly loop: Loop;
201
+ readonly stats: RoomStats;
202
+ tick: number;
203
+ stopped: boolean;
204
+ private readonly seed;
205
+ private readonly api;
206
+ private readonly internals;
207
+ private started;
208
+ constructor(definition: RoomDefinition<S>, host: RoomHost, options: RoomCoreOptions);
209
+ /** Convenience for hosts: `RoomCore.restore(def, bytes, host, opts)`. */
210
+ static restore<S2 extends AnySchema>(definition: RoomDefinition<S2>, bytes: Uint8Array, host: RoomHost, options: Omit<RoomCoreOptions, 'restoreFrom'>): RoomCore<S2>;
211
+ get schema(): S;
212
+ get config(): ResolvedRoomConfig<S>;
213
+ get state(): State<S>;
214
+ get room(): Room<S>;
215
+ tryRun<T>(name: string, fn: () => T): GuardResult<T>;
216
+ private readonly events;
217
+ recordEvent(kind: RoomEventKind, clientId?: string, detail?: string): void;
218
+ /** Live JSON view of the room for the dev page / supervisor admin API. */
219
+ inspect(): RoomInspection;
220
+ guard<T>(name: string, fn: () => T): T | undefined;
221
+ log(level: LogLevel, ...args: unknown[]): void;
222
+ start(): void;
223
+ stop(): void;
224
+ serialize(): Uint8Array;
225
+ private get presence();
226
+ private resolveRole;
227
+ join(clientId: string, options?: JoinOptions): JoinResult;
228
+ /** Event mode only: presence/lifecycle changes are their own event. No-op in tick mode. */
229
+ private eventFlush;
230
+ leave(clientId: string, reason: LeaveReason): void;
231
+ markDisconnected(clientId: string): void;
232
+ ctxFor(clientId: string, reconnecting?: boolean): Ctx;
233
+ correctionFor(clientId: string): DirtySet | undefined;
234
+ invalidateClients(): void;
235
+ send(clientId: string, frame: Uint8Array): void;
236
+ private badFrame;
237
+ receive(clientId: string, frame: Uint8Array): void;
238
+ /**
239
+ * Hands the tracked dirty set out: server-wins corrections first, then per connected client its
240
+ * pending `CORRECT` (before the delta, so it sees the correction and then the broadcast) and
241
+ * its view's `DELTA` — encoded once per distinct view.
242
+ */
243
+ flush(): void;
244
+ }
245
+
246
+ export { CRASH_AFTER_THROWS as C, MAX_CATCHUP as M, RoomCore as R, Mulberry32 as a };