@irtio/runtime 0.5.1 → 0.6.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.
@@ -1,6 +1,6 @@
1
- import { L as LogLevel, j as RoomStats, a as HostCall, b as HostCallResult, R as RoomCoreApi, h as RoomHost } from '../contract-B8QSO0MH.js';
2
- import { ErrorCodeName } from '@irtio/protocol';
3
- import { LeaveReason } from '@irtio/server';
1
+ import { L as LogLevel, T as TimelineDump, k as RoomStats, b as HostCall, c as HostCallResult, R as RoomCoreApi, i as RoomHost } from '../contract-BjMsoJIV.js';
2
+ import { ErrorCodeName, ProfileSnapshot } from '@irtio/protocol';
3
+ import { NpcConfig, LeaveReason } from '@irtio/server';
4
4
  import '@irtio/schema';
5
5
 
6
6
  /**
@@ -35,6 +35,11 @@ type ToWorker = {
35
35
  * the one `bundleUrl` implements.
36
36
  */
37
37
  migrate?: MigrationChainStep[];
38
+ /**
39
+ * D65: run this room with a bandwidth ledger. Absent means off, which is the shape every
40
+ * existing supervisor already sends, and a room without it pays nothing.
41
+ */
42
+ profile?: boolean;
38
43
  } | {
39
44
  t: 'join';
40
45
  clientId: string;
@@ -44,6 +49,8 @@ type ToWorker = {
44
49
  * `ctx.playerId`. Absent for key joins, whose playerId stays the client id. */
45
50
  playerId?: string;
46
51
  reconnecting?: boolean;
52
+ /** D44: this join is a scripted NPC's loopback session. */
53
+ npc?: boolean;
47
54
  } | {
48
55
  t: 'leave';
49
56
  clientId: string;
@@ -65,8 +72,29 @@ type ToWorker = {
65
72
  } | {
66
73
  t: 'inspect';
67
74
  reqId: number;
75
+ }
76
+ /**
77
+ * D41: arm (or re-arm) the authoritative timeline recorder, and read it back. Two messages
78
+ * rather than a flag on `inspect`, because recording is a room-lifetime decision and reading is
79
+ * a per-request one. Both are additive: a supervisor that never sends them costs a room nothing.
80
+ */
81
+ | {
82
+ t: 'record';
83
+ reqId: number;
84
+ maxTicks?: number;
85
+ maxRecords?: number;
68
86
  } | {
87
+ t: 'timeline';
88
+ reqId: number;
89
+ }
90
+ /**
91
+ * D36: `reqId` correlates the answer, so a caller can *await* one room's counters instead of
92
+ * waiting for a periodic push. Optional and echoed back when present, so a host that asks
93
+ * without one still gets the uncorrelated message it expects.
94
+ */
95
+ | {
69
96
  t: 'stats';
97
+ reqId?: number;
70
98
  }
71
99
  /**
72
100
  * Week 12: the supervisor answering a `hostCall` (a D24 save, a D25 KV operation). It arrives
@@ -82,6 +110,28 @@ type ToWorker = {
82
110
  | {
83
111
  t: 'alarm';
84
112
  name: string;
113
+ }
114
+ /**
115
+ * D59: a published message reaching a channel this room is subscribed to. A separate message
116
+ * from `alarm` because an alarm carries only a name, and the whole point of the bus is the
117
+ * payload. `from` is stamped by the supervisor off the publishing room's own record, so a room
118
+ * cannot forge a source.
119
+ */
120
+ | {
121
+ t: 'busEvent';
122
+ channel: string;
123
+ from: string;
124
+ payload: string;
125
+ }
126
+ /**
127
+ * D59: a directed `room.bus.send` being delivered. At-least-once: the supervisor re-arms an
128
+ * entry it could not deliver, so the same message can arrive twice.
129
+ */
130
+ | {
131
+ t: 'busMessage';
132
+ name: string;
133
+ from: string;
134
+ payload: string;
85
135
  } | {
86
136
  t: 'stop';
87
137
  };
@@ -98,6 +148,12 @@ type FromWorker = {
98
148
  idleMs: number;
99
149
  reconnectGraceMs: number;
100
150
  maxClients: number;
151
+ /** D63-e: the room type's declared backfill eligibility. The ceiling on
152
+ * `room.backfill.set`, and `false` for every room that declares nothing. */
153
+ backfill?: boolean;
154
+ /** D47: the physics engine this room runs, when it declares one. Reported so room-hours
155
+ * rows carry the engine as a dimension; the supervisor never acts on it. */
156
+ engine?: string;
101
157
  };
102
158
  /** Present when `init.migrate` ran; the supervisor re-snapshots on the strength of it. */
103
159
  migrated?: {
@@ -158,11 +214,56 @@ type FromWorker = {
158
214
  }[];
159
215
  /** Worker process RSS in bytes. */
160
216
  rss: number;
217
+ }
218
+ /** D41: the recorder is armed. `ok: false` when there is no room core to arm. */
219
+ | {
220
+ t: 'recording';
221
+ reqId: number;
222
+ ok: boolean;
223
+ }
224
+ /** D41: the recorded timeline. `dump` absent when nobody armed the recorder. */
225
+ | {
226
+ t: 'timeline';
227
+ reqId: number;
228
+ dump?: TimelineDump;
161
229
  } | {
162
230
  t: 'stats';
231
+ /** Echoed from the request when it carried one (D36). */
232
+ reqId?: number;
163
233
  stats: Omit<RoomStats, 'bytesOutByClient'> & {
164
234
  bytesOutByClient: [string, number][];
165
235
  };
236
+ /**
237
+ * D55: the worker thread's own V8 heap, sampled at reply time. It rides this existing
238
+ * round trip rather than adding a protocol surface, and it is deliberately not part of
239
+ * `RoomStats` — `RoomStats` is the room's own counters, and this is a fact about the
240
+ * thread the room happens to be running in.
241
+ *
242
+ * `limitBytes` is V8's `heap_size_limit`, which inside a worker reflects the
243
+ * `resourceLimits` the host spawned it with, so the supervisor does not have to remember
244
+ * what cap it handed out to know how close the room is to it. Absent when the worker's
245
+ * V8 could not be sampled, which the supervisor must report as unread rather than as
246
+ * zero, for the same reason `ticks`/`overruns` are optional.
247
+ */
248
+ heap?: {
249
+ usedBytes: number;
250
+ limitBytes: number;
251
+ };
252
+ /**
253
+ * D56: `true` once any Rapier world in this worker process has stepped, which is the point
254
+ * V8 has tier-compiled the physics WASM and paid its RSS transient. It rides `stats`
255
+ * because that round trip already exists; the worker also pushes one uncorrelated `stats`
256
+ * the first time it flips, so the supervisor learns it without having to be polling at the
257
+ * right moment. Absent means "not yet" (or an older worker), never "no".
258
+ */
259
+ physicsStepped?: boolean;
260
+ /**
261
+ * D65: the room's bandwidth ledger, when it was started with one. It rides `stats` for the
262
+ * same reason D55's `heap` does — the round trip already exists — and stays off
263
+ * `RoomStats`, which is a fixed shape the harness and the hibernation path both pin.
264
+ * Absent means "this room is not profiling", never "no bytes".
265
+ */
266
+ profile?: ProfileSnapshot;
166
267
  }
167
268
  /** Week 12: the room asked for host-side async work; the supervisor answers `hostResult`. */
168
269
  | {
@@ -179,6 +280,58 @@ type FromWorker = {
179
280
  t: 'setAlarm';
180
281
  name: string;
181
282
  atMs?: number;
283
+ }
284
+ /**
285
+ * D63-e: `room.backfill.set(open)`. Fire-and-forget: the room gets no result, because the only
286
+ * thing that could go wrong is that the room never declared `backfill` at all, which is a fact
287
+ * the room already knows about itself.
288
+ */
289
+ | {
290
+ t: 'setBackfill';
291
+ open: boolean;
292
+ }
293
+ /**
294
+ * D59: `room.bus.publish(channel, payload)`. Fire-and-forget by design — the room gets no
295
+ * result, because the only honest answer is "delivered to whoever happened to be awake", and a
296
+ * refusal (an oversized payload, a rate limit) comes back as a log line rather than as a value
297
+ * the room would have no useful response to.
298
+ */
299
+ | {
300
+ t: 'busPublish';
301
+ channel: string;
302
+ payload: string;
303
+ }
304
+ /** D59: `room.bus.subscribe`/`unsubscribe`, and the automatic subscribe of declared channels. */
305
+ | {
306
+ t: 'busSubscribe';
307
+ channel: string;
308
+ subscribed: boolean;
309
+ }
310
+ /**
311
+ * D59: the room ran (or knowingly discarded) the mailbox entry called `name`.
312
+ *
313
+ * This is what clears the entry. The supervisor re-arms an entry for a visibility timeout at the
314
+ * moment it posts the `busMessage`, so a delivery that is posted but never runs comes due again
315
+ * instead of vanishing. Without this ack, at-least-once would be at-most-once wearing its name.
316
+ */
317
+ | {
318
+ t: 'busAck';
319
+ name: string;
320
+ }
321
+ /**
322
+ * D44: `room.spawnNPC(config)` / the handle's `despawn()`. The room minted `clientId`, so the
323
+ * handle is usable synchronously; the supervisor opens the loopback session under it and runs
324
+ * the named entry of the definition's `npcs` map — which it can, because it imported the same
325
+ * bundle to read the config out of. Only the *name* crosses this boundary; the function stays
326
+ * on the side that can call it. Additive, like every other message here.
327
+ */
328
+ | {
329
+ t: 'spawnNpc';
330
+ clientId: string;
331
+ config: NpcConfig;
332
+ } | {
333
+ t: 'despawnNpc';
334
+ clientId: string;
182
335
  } | {
183
336
  t: 'stopped';
184
337
  };
@@ -1,13 +1,17 @@
1
1
  import {
2
2
  migrateSnapshot
3
- } from "../chunk-HFOMXKSO.js";
3
+ } from "../chunk-VMCE3LRO.js";
4
4
  import {
5
5
  RoomCore,
6
6
  RoomFullError,
7
- initPhysics
8
- } from "../chunk-5ZDQAAFJ.js";
7
+ initMatter,
8
+ initPhysics,
9
+ onFirstRapierStep,
10
+ rapierHasStepped
11
+ } from "../chunk-K42HA75G.js";
9
12
 
10
13
  // src/worker/index.ts
14
+ import { getHeapStatistics } from "v8";
11
15
  import { isMainThread, parentPort } from "worker_threads";
12
16
  import { isRoomDefinition } from "@irtio/server";
13
17
 
@@ -60,6 +64,21 @@ function createWorkerHost(post) {
60
64
  hostCall(reqId, call) {
61
65
  post({ t: "hostCall", reqId, call });
62
66
  },
67
+ spawnNpc(clientId, config) {
68
+ post({ t: "spawnNpc", clientId, config });
69
+ },
70
+ despawnNpc(clientId) {
71
+ post({ t: "despawnNpc", clientId });
72
+ },
73
+ busPublish(channel, payload) {
74
+ post({ t: "busPublish", channel, payload });
75
+ },
76
+ busSubscribe(channel, subscribed) {
77
+ post({ t: "busSubscribe", channel, subscribed });
78
+ },
79
+ setBackfill(open) {
80
+ post({ t: "setBackfill", open });
81
+ },
63
82
  setAlarm(name, atMs) {
64
83
  if (atMs === void 0) {
65
84
  post({ t: "setAlarm", name });
@@ -91,9 +110,28 @@ async function loadChain(steps) {
91
110
  }
92
111
  return out;
93
112
  }
94
- function statsPayload(stats) {
113
+ function heapSample() {
114
+ try {
115
+ const s = getHeapStatistics();
116
+ if (!Number.isFinite(s.used_heap_size) || !Number.isFinite(s.heap_size_limit)) return void 0;
117
+ if (s.heap_size_limit <= 0) return void 0;
118
+ return { usedBytes: s.used_heap_size, limitBytes: s.heap_size_limit };
119
+ } catch {
120
+ return void 0;
121
+ }
122
+ }
123
+ function statsPayload(stats, reqId, profile) {
95
124
  const { bytesOutByClient, ...rest } = stats;
96
- return { t: "stats", stats: { ...rest, bytesOutByClient: [...bytesOutByClient.entries()] } };
125
+ const heap = heapSample();
126
+ return {
127
+ t: "stats",
128
+ ...reqId !== void 0 ? { reqId } : {},
129
+ stats: { ...rest, bytesOutByClient: [...bytesOutByClient.entries()] },
130
+ ...heap !== void 0 ? { heap } : {},
131
+ ...rapierHasStepped() ? { physicsStepped: true } : {},
132
+ // D65: absent unless this room was started with a ledger.
133
+ ...profile !== void 0 ? { profile } : {}
134
+ };
97
135
  }
98
136
  async function handleMessage(state, host, post, msg) {
99
137
  try {
@@ -113,7 +151,14 @@ async function handleMessage(state, host, post, msg) {
113
151
  }
114
152
  if (def.config.physics) {
115
153
  try {
116
- await initPhysics();
154
+ if (def.config.physics.engine === "matter2d") await initMatter();
155
+ else {
156
+ await initPhysics();
157
+ onFirstRapierStep(() => {
158
+ const core = state.core;
159
+ if (core) post(statsPayload(core.stats, void 0, core.profile()));
160
+ });
161
+ }
117
162
  } catch (err) {
118
163
  post({
119
164
  t: "initFailed",
@@ -144,7 +189,8 @@ async function handleMessage(state, host, post, msg) {
144
189
  const options = {
145
190
  roomId: msg.roomId,
146
191
  ...msg.seed !== void 0 ? { seed: msg.seed } : {},
147
- ...msg.publicUrl !== void 0 ? { publicUrl: msg.publicUrl } : {}
192
+ ...msg.publicUrl !== void 0 ? { publicUrl: msg.publicUrl } : {},
193
+ ...msg.profile === true ? { profile: true } : {}
148
194
  };
149
195
  const core = snapshot ? RoomCore.restore(def, snapshot, host, options) : new RoomCore(def, host, options);
150
196
  state.core = core;
@@ -160,7 +206,16 @@ async function handleMessage(state, host, post, msg) {
160
206
  tickRate: c.tickRate,
161
207
  idleMs: c.idleMs,
162
208
  reconnectGraceMs: c.reconnectGraceMs,
163
- maxClients: c.maxClients
209
+ maxClients: c.maxClients,
210
+ // D63-e: the declared ceiling, carried on the same frame and for the same reason
211
+ // `engine` is: `ready.config` replaces the supervisor's copy, so a flag threaded
212
+ // only through `loadBundle` would be dropped the moment a worker started.
213
+ backfill: c.backfill === true,
214
+ // D47: `ready.config` REPLACES the supervisor's copy of the bundle config, so an
215
+ // engine threaded only through `loadBundle` would be dropped here the moment a
216
+ // worker started. That is exactly how the first known-shape run read `engine: none`
217
+ // against a matter2d room.
218
+ ...typeof c.physics?.engine === "string" ? { engine: c.physics.engine } : {}
164
219
  },
165
220
  ...migrated ? {
166
221
  migrated: {
@@ -192,7 +247,8 @@ async function handleMessage(state, host, post, msg) {
192
247
  ...msg.role !== void 0 ? { role: msg.role } : {},
193
248
  ...msg.name !== void 0 ? { name: msg.name } : {},
194
249
  ...msg.playerId !== void 0 ? { playerId: msg.playerId } : {},
195
- ...msg.reconnecting !== void 0 ? { reconnecting: msg.reconnecting } : {}
250
+ ...msg.reconnecting !== void 0 ? { reconnecting: msg.reconnecting } : {},
251
+ ...msg.npc === true ? { npc: true } : {}
196
252
  };
197
253
  const result = core.join(msg.clientId, joinOptions);
198
254
  const snapshot = ownBuffer(result.snapshot);
@@ -253,10 +309,28 @@ async function handleMessage(state, host, post, msg) {
253
309
  });
254
310
  return;
255
311
  }
312
+ case "record": {
313
+ const core = state.core;
314
+ if (!core) {
315
+ post({ t: "recording", reqId: msg.reqId, ok: false });
316
+ return;
317
+ }
318
+ core.startRecording({
319
+ ...msg.maxTicks !== void 0 ? { maxTicks: msg.maxTicks } : {},
320
+ ...msg.maxRecords !== void 0 ? { maxRecords: msg.maxRecords } : {}
321
+ });
322
+ post({ t: "recording", reqId: msg.reqId, ok: true });
323
+ return;
324
+ }
325
+ case "timeline": {
326
+ const dump = state.core?.recording();
327
+ post({ t: "timeline", reqId: msg.reqId, ...dump !== void 0 ? { dump } : {} });
328
+ return;
329
+ }
256
330
  case "stats": {
257
331
  const core = state.core;
258
332
  if (!core) return;
259
- post(statsPayload(core.stats));
333
+ post(statsPayload(core.stats, msg.reqId, core.profile()));
260
334
  return;
261
335
  }
262
336
  case "hostResult": {
@@ -267,6 +341,18 @@ async function handleMessage(state, host, post, msg) {
267
341
  state.core?.fireAlarm(msg.name);
268
342
  return;
269
343
  }
344
+ case "busEvent": {
345
+ state.core?.deliverBusEvent(msg.channel, msg.from, msg.payload);
346
+ return;
347
+ }
348
+ case "busMessage": {
349
+ try {
350
+ state.core?.deliverBusMessage(msg.from, msg.payload);
351
+ } finally {
352
+ post({ t: "busAck", name: msg.name });
353
+ }
354
+ return;
355
+ }
270
356
  case "stop": {
271
357
  state.core?.stop();
272
358
  post({ t: "stopped" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/runtime",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -29,9 +29,13 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@dimforge/rapier3d-compat": "0.20.0",
32
- "@irtio/schema": "0.5.1",
33
- "@irtio/server": "0.5.1",
34
- "@irtio/protocol": "0.5.1"
32
+ "matter-js": "0.20.0",
33
+ "@irtio/protocol": "0.6.0",
34
+ "@irtio/schema": "0.6.0",
35
+ "@irtio/server": "0.6.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/matter-js": "0.20.2"
35
39
  },
36
40
  "scripts": {
37
41
  "build": "tsup",
@@ -1,204 +0,0 @@
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' | 'alarm' | '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
- /**
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;
66
- /** Everything the room needs from the outside world. No timers, sockets, clocks, or console inside. */
67
- interface RoomHost {
68
- /** Monotonic ms. Fake in the harness. */
69
- now(): number;
70
- setTimeout(fn: () => void, ms: number): unknown;
71
- clearTimeout(handle: unknown): void;
72
- /** Deliver one fully framed protocol frame (type byte + payload) to a client. */
73
- send(clientId: string, frame: Uint8Array): void;
74
- /** The room wants this client gone (`room.kick`, protocol violation). Host closes the socket. */
75
- kick(clientId: string, code: ErrorCodeName, reason?: string): void;
76
- /** `room.close()`: host disconnects everyone and tears the room down. */
77
- close(reason?: string): void;
78
- /** Event mode: idle or `room.sleep()` — host should `serialize()` then `stop()`. */
79
- sleep(): void;
80
- log(level: LogLevel, args: unknown[]): void;
81
- /** N consecutive tick throws; the runtime does not recover itself. */
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;
96
- }
97
- interface RoomCoreOptions {
98
- readonly roomId: string;
99
- /** Seed for `room.random()`; default 1. */
100
- readonly seed?: number;
101
- /** Base URL for `room.link` (`<publicUrl>?room=<id>`); default `http://localhost/`. */
102
- readonly publicUrl?: string;
103
- /** Bytes from a previous `serialize()`; skips `onCreate`, runs `onWake`. */
104
- readonly restoreFrom?: Uint8Array;
105
- }
106
- interface JoinOptions {
107
- /** Requested role; unknown/missing → `roles[0]` (logged) or `''` when the schema has no roles. */
108
- readonly role?: string;
109
- readonly name?: string;
110
- /** A resumed session: presence record kept, `ctx.reconnecting = true`. */
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;
119
- }
120
- interface JoinResult {
121
- readonly tick: number;
122
- readonly role: string;
123
- /** Snapshot bytes for this client's view (extended schema). The host builds `WELCOME`. */
124
- readonly snapshot: Uint8Array;
125
- }
126
- declare class RoomFullError extends Error {
127
- readonly maxClients: number;
128
- readonly name = "RoomFullError";
129
- constructor(maxClients: number);
130
- }
131
- interface RoomStats {
132
- ticks: number;
133
- lastTickMs: number;
134
- maxTickMs: number;
135
- overruns: number;
136
- framesIn: number;
137
- framesOut: number;
138
- bytesOut: number;
139
- /** Per connected client. */
140
- bytesOutByClient: Map<string, number>;
141
- handlerErrors: number;
142
- encodesLastFlush: number;
143
- aoiEncodesLastFlush: number;
144
- gridBuildMs: number;
145
- gridQueryMs: number;
146
- aoiEncodeMs: number;
147
- visibleIdsTotal: number;
148
- membershipEnters: number;
149
- membershipLeaves: number;
150
- corrections: number;
151
- }
152
- /** The surface the worker host and the harness drive. Implemented by `RoomCore`. */
153
- interface RoomCoreApi<S extends AnySchema = AnySchema> {
154
- readonly definition: RoomDefinition<S>;
155
- /** The builder's schema. */
156
- readonly schema: S;
157
- /** `withBuiltins(schema)`: what the codec and every frame use. */
158
- readonly ext: AnySchema;
159
- readonly roomId: string;
160
- readonly tick: number;
161
- /** Tracked authority (proxy tree incl. the built-in `clients` collection). */
162
- readonly state: State<S>;
163
- readonly tracked: Tracked<AnySchema>;
164
- /** The `room` API object handed to handlers. */
165
- readonly room: Room<S>;
166
- readonly stats: RoomStats;
167
- readonly mode: 'tick' | 'event';
168
- /** Begin ticking (tick mode) / idle timer (event mode). Idempotent. */
169
- start(): void;
170
- /** Stop timers; no more frames are sent. */
171
- stop(): void;
172
- /** Throws `RoomFullError`. Runs `onJoin`, then snapshots the client's view. */
173
- join(clientId: string, options?: JoinOptions): JoinResult;
174
- /** Runs `onLeave`, removes presence, rejects pending RPCs for the client. */
175
- leave(clientId: string, reason: LeaveReason): void;
176
- /** Grace window opened by the host: presence `connected = false`; frames are no longer sent. */
177
- markDisconnected(clientId: string): void;
178
- /** One inbound protocol frame (`WRITE` | `CALL` | `REPLY` | `MSG`). Anything else → host.kick(E_BAD_FRAME). */
179
- receive(clientId: string, frame: Uint8Array): void;
180
- /** Full state + tick + rng for hibernation. Runs `onSleep`. Clears timers. */
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;
188
- /** Live JSON view of the room (dev page / supervisor admin API). */
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;
202
- }
203
-
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 };