@irtio/runtime 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import { RoomDefinition, Room, LeaveReason } from '@irtio/server';
7
7
  * the room's state plus a ring of recent events. Always on; recording is O(1) per event.
8
8
  */
9
9
 
10
- type RoomEventKind = 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'error';
10
+ type RoomEventKind = 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'alarm' | 'error';
11
11
  interface RoomEvent {
12
12
  readonly tick: number;
13
13
  readonly kind: RoomEventKind;
@@ -24,6 +24,45 @@ interface RoomInspection {
24
24
  }
25
25
 
26
26
  type LogLevel = 'info' | 'warn' | 'error';
27
+ /**
28
+ * Week 12: host-side async work a room asked for — a save generation (D24) or a player-KV
29
+ * operation (D25). Room handlers are synchronous and there is no way to block one on I/O, so
30
+ * every one of these is started by `RoomHost.hostCall` and *finished* by the host calling
31
+ * `RoomCoreApi.completeHostCall` — the same way an inbound frame arrives. That is what makes
32
+ * "the continuation runs as its own event between ticks" a structural property of the seam
33
+ * rather than a promise the runtime makes and hopes the host keeps.
34
+ */
35
+ type HostCall = {
36
+ readonly kind: 'save';
37
+ } | {
38
+ readonly kind: 'kvGet';
39
+ readonly playerId: string;
40
+ readonly key: string;
41
+ } | {
42
+ readonly kind: 'kvSet';
43
+ readonly playerId: string;
44
+ readonly key: string;
45
+ readonly value: string;
46
+ } | {
47
+ readonly kind: 'kvDelete';
48
+ readonly playerId: string;
49
+ readonly key: string;
50
+ };
51
+ /**
52
+ * What a `HostCall` produced. `value` carries the save id for `save` and the stored string for
53
+ * `kvGet` (absent = the key was never written, which resolves `undefined` rather than rejecting).
54
+ * A failure carries a named `E_*` code so the room can tell a size limit from an outage.
55
+ */
56
+ type HostCallResult = {
57
+ readonly ok: true;
58
+ readonly value?: string;
59
+ } | {
60
+ readonly ok: false;
61
+ readonly code: string;
62
+ readonly message: string;
63
+ };
64
+ /** How long a `HostCall` may stay outstanding before the runtime rejects it itself. */
65
+ declare const HOST_CALL_TIMEOUT_MS = 10000;
27
66
  /** Everything the room needs from the outside world. No timers, sockets, clocks, or console inside. */
28
67
  interface RoomHost {
29
68
  /** Monotonic ms. Fake in the harness. */
@@ -41,6 +80,19 @@ interface RoomHost {
41
80
  log(level: LogLevel, args: unknown[]): void;
42
81
  /** N consecutive tick throws; the runtime does not recover itself. */
43
82
  crashed(reason: string): void;
83
+ /**
84
+ * Week 12: start host-side async work (D24 save, D25 KV). The host **must** deliver the
85
+ * outcome by calling `RoomCoreApi.completeHostCall(reqId, result)` from its own event loop
86
+ * turn, never synchronously from inside this call — a handler is on the stack.
87
+ */
88
+ hostCall(reqId: number, call: HostCall): void;
89
+ /**
90
+ * D26: arm the durable alarm `name` for `atMs`, or cancel it when `atMs` is `undefined`.
91
+ * Arming a name that is already armed replaces its due time. Alarm state is the **host's**,
92
+ * not the room's: nothing about it goes into the hibernation blob, and the host fires one by
93
+ * calling `RoomCoreApi.fireAlarm(name)`.
94
+ */
95
+ setAlarm(name: string, atMs: number | undefined): void;
44
96
  }
45
97
  interface RoomCoreOptions {
46
98
  readonly roomId: string;
@@ -57,6 +109,13 @@ interface JoinOptions {
57
109
  readonly name?: string;
58
110
  /** A resumed session: presence record kept, `ctx.reconnecting = true`. */
59
111
  readonly reconnecting?: boolean;
112
+ /**
113
+ * D25: the identity `ctx.playerId` reports and `room.kv` is meant to be keyed by. Defaults to
114
+ * `clientId`, which — because a resume token carries the client id across a reconnect — is
115
+ * exactly the resume-token identity, and no more stable than that token. Week 13 (D27) passes
116
+ * a JWT subject here instead without anything else changing.
117
+ */
118
+ readonly playerId?: string;
60
119
  }
61
120
  interface JoinResult {
62
121
  readonly tick: number;
@@ -81,6 +140,13 @@ interface RoomStats {
81
140
  bytesOutByClient: Map<string, number>;
82
141
  handlerErrors: number;
83
142
  encodesLastFlush: number;
143
+ aoiEncodesLastFlush: number;
144
+ gridBuildMs: number;
145
+ gridQueryMs: number;
146
+ aoiEncodeMs: number;
147
+ visibleIdsTotal: number;
148
+ membershipEnters: number;
149
+ membershipLeaves: number;
84
150
  corrections: number;
85
151
  }
86
152
  /** The surface the worker host and the harness drive. Implemented by `RoomCore`. */
@@ -113,8 +179,26 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
113
179
  receive(clientId: string, frame: Uint8Array): void;
114
180
  /** Full state + tick + rng for hibernation. Runs `onSleep`. Clears timers. */
115
181
  serialize(): Uint8Array;
182
+ /**
183
+ * D24: the same bytes `serialize()` produces, with none of its side effects — the room carries
184
+ * on exactly as it was. This is what a save generation is written from; `serialize()` is what
185
+ * a room going to sleep is written from.
186
+ */
187
+ snapshot(): Uint8Array;
116
188
  /** Live JSON view of the room (dev page / supervisor admin API). */
117
189
  inspect(): RoomInspection;
190
+ /**
191
+ * Week 12: settles the promise `hostCall(reqId, …)` returned. The host calls this from its own
192
+ * turn; the runtime runs the continuation as a discrete event and flushes afterwards, so state
193
+ * a continuation mutated goes out exactly like state a handler mutated.
194
+ */
195
+ completeHostCall(reqId: number, result: HostCallResult): void;
196
+ /**
197
+ * D26: runs the `alarms[name]` handler as its own event between ticks, then flushes. Unknown
198
+ * names are logged and ignored — a room that dropped a handler in a redeploy must not crash on
199
+ * an alarm armed by the version before it.
200
+ */
201
+ fireAlarm(name: string): void;
118
202
  }
119
203
 
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 };
204
+ export { EVENT_RING_SIZE as E, HOST_CALL_TIMEOUT_MS as H, type JoinOptions as J, type LogLevel as L, type RoomCoreApi as R, type HostCall as a, type HostCallResult as b, type JoinResult as c, type RoomCoreOptions as d, type RoomEvent as e, type RoomEventKind as f, RoomFullError as g, type RoomHost as h, type RoomInspection as i, type RoomStats as j, inspectState as k };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
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';
1
+ export { E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, a as HostCall, b as HostCallResult, J as JoinOptions, c as JoinResult, L as LogLevel, R as RoomCoreApi, d as RoomCoreOptions, e as RoomEvent, f as RoomEventKind, g as RoomFullError, h as RoomHost, i as RoomInspection, j as RoomStats, k as inspectState } from './contract-B8QSO0MH.js';
2
+ export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as Mulberry32, P as PhysicsSection, R as RoomCore, d as decodePhysicsSection, e as encodePhysicsSection, i as initPhysics, l as loadedPhysics, r as resetPhysicsForTests } from './room-BfALTh7M.js';
3
+ import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
4
4
  import '@irtio/protocol';
5
5
  import '@irtio/server';
6
6
 
@@ -40,7 +40,16 @@ declare function visibleNames(ext: AnySchema, role: string): ReadonlySet<string>
40
40
  */
41
41
  declare function viewKeyFor(ext: AnySchema, role: string): string;
42
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;
43
+ declare function encodeViewSnapshot(ext: AnySchema, plain: PlainState, role: string, tick: number, memberships?: ReadonlyMap<string, ReadonlySet<string>>): Uint8Array;
44
+ interface VisibilityPolicy {
45
+ readonly memberships: ReadonlyMap<string, ReadonlySet<string>>;
46
+ maySeeCollection(desc: CollectionDesc): boolean;
47
+ maySeeEntity(desc: CollectionDesc, id: string): boolean;
48
+ /** Viewer cell, offending cells, and the radius — the diagnostics a leak report needs. */
49
+ describeSpatial(desc: CollectionDesc, ids: readonly string[]): string | undefined;
50
+ }
51
+ /** Reusable authority-based policy for runtime, harness, and bot leak checks. */
52
+ declare function createVisibilityPolicy(ext: AnySchema, plain: PlainState, clientId: string, role: string): VisibilityPolicy;
44
53
  /** One view's `DELTA` payload, or `null` when nothing visible to `role` changed. */
45
54
  declare function encodeViewDelta(ext: AnySchema, plain: PlainState, dirty: DirtySet, role: string, tick: number): Uint8Array | null;
46
55
  /**
@@ -61,14 +70,28 @@ declare function catchUpDirty(ext: AnySchema, plain: PlainState, fromRole: strin
61
70
  * u32 rng state
62
71
  * u32 tick
63
72
  * u8 mode (1 = event, 0 = tick)
73
+ * [v2] blob physics section (varint length + bytes; length 0 = no world)
64
74
  * ... encodeSnapshot(withBuiltins(schema), state)
65
75
  *
76
+ * **v2 (week 9, D22)** adds the physics section: the Rapier world snapshot plus the entity↔handle
77
+ * map (`core/physics.ts` encodes it). It rides *inside* this blob rather than beside it so that
78
+ * wake is atomic — state and world always come back from the same bytes, and every store, cache
79
+ * and transfer path that already moves one array keeps working untouched.
80
+ *
81
+ * A **v1 blob still reads**: it simply has no physics section. For a room that has since gained
82
+ * physics that is the pre-physics-deploy case — the world is rebuilt from schema state and the
83
+ * room's `setup` runs again, which is logged. Writers emit v1 when there is no world, so a room
84
+ * without physics produces byte-identical blobs to the ones week 8 wrote.
85
+ *
66
86
  * Note: the *deployment* version is deliberately **not** in here. It lives in the store
67
87
  * key (`<project>/<room>@v<deployment>`), which keeps the blob format frozen — an old snapshot
68
88
  * stays valid across deployments — and lets a wake path discover the version by listing keys
69
89
  * instead of parsing bytes it may not yet know how to read.
70
90
  */
71
- declare const SNAPSHOT_FORMAT_VERSION = 1;
91
+ /** Written when the blob carries a physics world; v1 otherwise (and v1 is still read). */
92
+ declare const SNAPSHOT_FORMAT_VERSION = 2;
93
+ /** Versions this build can parse. */
94
+ declare const READABLE_SNAPSHOT_VERSIONS: readonly number[];
72
95
  type SnapshotMode = 'tick' | 'event';
73
96
  interface SnapshotHeader {
74
97
  readonly seed: number;
@@ -77,11 +100,15 @@ interface SnapshotHeader {
77
100
  readonly mode: SnapshotMode;
78
101
  }
79
102
  interface ParsedSnapshot extends SnapshotHeader {
103
+ /** The blob's own format version (1 or 2). */
104
+ readonly version: number;
105
+ /** The encoded physics section, or `undefined` for a v1 blob / a room with no world. */
106
+ readonly physics: Uint8Array | undefined;
80
107
  /** The codec snapshot, encoded under `withBuiltins(schema)`. */
81
108
  readonly snapshot: Uint8Array;
82
109
  }
83
110
  declare function parseHibernationBlob(bytes: Uint8Array): ParsedSnapshot;
84
- declare function writeHibernationBlob(header: SnapshotHeader, snapshot: Uint8Array): Uint8Array;
111
+ declare function writeHibernationBlob(header: SnapshotHeader, snapshot: Uint8Array, physics?: Uint8Array): Uint8Array;
85
112
 
86
113
  /**
87
114
  * Wake-time schema migration.
@@ -123,7 +150,11 @@ interface MigrationHelpers {
123
150
  }
124
151
  interface MigrationModule {
125
152
  up(state: MigrationState, s: MigrationHelpers): MigrationState | void;
126
- /** Scaffolded by `irtio migrate create` and stored; unused until rollback ships. */
153
+ /**
154
+ * Scaffolded by `irtio migrate create` and stored, but never called. `irtio rollback` restores
155
+ * the pre-migration save generation the `migrate` deploy wrote rather than running an inverse
156
+ * transform, which is how it undoes migrations that have no `down` at all.
157
+ */
127
158
  down?(state: MigrationState, s: MigrationHelpers): MigrationState | void;
128
159
  }
129
160
  /**
@@ -158,4 +189,4 @@ declare function migrateSnapshot(blob: Uint8Array, chain: readonly MigrationStep
158
189
  declare function toMigrationState(schema: AnySchema, plain: PlainState): MigrationState;
159
190
  declare function fromMigrationState(schema: AnySchema, state: MigrationState, version: number): PlainState;
160
191
 
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 };
192
+ export { 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, encodeViewDelta, encodeViewSnapshot, fromMigrationState, isVisible, migrateSnapshot, parseHibernationBlob, toMigrationState, viewKeyFor, visibleNames, visibleTo, writeHibernationBlob };
package/dist/index.js CHANGED
@@ -2,44 +2,60 @@ import {
2
2
  fromMigrationState,
3
3
  migrateSnapshot,
4
4
  toMigrationState
5
- } from "./chunk-3DQHT4CM.js";
5
+ } from "./chunk-FA4IOC2Z.js";
6
6
  import {
7
7
  CRASH_AFTER_THROWS,
8
8
  EVENT_RING_SIZE,
9
+ HOST_CALL_TIMEOUT_MS,
9
10
  MAX_CATCHUP,
10
11
  Mulberry32,
12
+ READABLE_SNAPSHOT_VERSIONS,
11
13
  RPC_TIMEOUT_MS,
12
14
  RoomCore,
13
15
  RoomFullError,
14
16
  SNAPSHOT_FORMAT_VERSION,
15
17
  catchUpDirty,
18
+ createVisibilityPolicy,
19
+ decodePhysicsSection,
20
+ encodePhysicsSection,
16
21
  encodeViewDelta,
17
22
  encodeViewSnapshot,
23
+ initPhysics,
18
24
  inspectState,
19
25
  isVisible,
26
+ loadedPhysics,
20
27
  parseHibernationBlob,
28
+ resetPhysicsForTests,
21
29
  viewKeyFor,
22
30
  visibleNames,
23
31
  visibleTo,
24
32
  writeHibernationBlob
25
- } from "./chunk-X5S364FY.js";
33
+ } from "./chunk-GKMD3ICD.js";
26
34
  export {
27
35
  CRASH_AFTER_THROWS,
28
36
  EVENT_RING_SIZE,
37
+ HOST_CALL_TIMEOUT_MS,
29
38
  MAX_CATCHUP,
30
39
  Mulberry32,
40
+ READABLE_SNAPSHOT_VERSIONS,
31
41
  RPC_TIMEOUT_MS,
32
42
  RoomCore,
33
43
  RoomFullError,
34
44
  SNAPSHOT_FORMAT_VERSION,
35
45
  catchUpDirty,
46
+ createVisibilityPolicy,
47
+ decodePhysicsSection,
48
+ encodePhysicsSection,
36
49
  encodeViewDelta,
37
50
  encodeViewSnapshot,
38
51
  fromMigrationState,
52
+ initPhysics,
39
53
  inspectState,
40
54
  isVisible,
55
+ loadedPhysics,
41
56
  migrateSnapshot,
42
57
  parseHibernationBlob,
58
+ resetPhysicsForTests,
43
59
  toMigrationState,
44
60
  viewKeyFor,
45
61
  visibleNames,