@irtio/runtime 0.5.2 → 0.7.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-5ZDQAAFJ.js → chunk-EXPFVRD4.js} +1572 -121
- package/dist/{chunk-HFOMXKSO.js → chunk-ZNNTF7Y3.js} +1 -1
- package/dist/contract-DwxqjeWP.d.ts +426 -0
- package/dist/index.d.ts +41 -3
- package/dist/index.js +26 -2
- package/dist/{room-9ZQoy9yi.d.ts → room-CoQDczh2.d.ts} +259 -7
- package/dist/test/index.d.ts +51 -4
- package/dist/test/index.js +70 -1
- package/dist/worker/index.d.ts +147 -3
- package/dist/worker/index.js +92 -10
- package/package.json +8 -4
- package/dist/contract-B8QSO0MH.d.ts +0 -204
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { AttributeOptions, ProfileSnapshot } from '@irtio/protocol';
|
|
1
2
|
import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
|
|
2
|
-
import { RoomDefinition, RoomMode, Room, Ctx, RapierModule, RapierWorld, RapierRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
|
|
3
|
-
import {
|
|
3
|
+
import { RoomDefinition, RoomMode, Room, RewindView, Ctx, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
|
|
4
|
+
import { i as RoomHost, k as RoomStats, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, g as RoomEventKind, n as TimelineRecorderOptions, T as TimelineDump, j as RoomInspection, J as JoinOptions, d as JoinResult, c as HostCallResult } from './contract-DwxqjeWP.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `room.random()` — mulberry32 over a u32 seed. Deterministic, tiny, and serializable: the
|
|
@@ -27,6 +28,13 @@ type AcceptedWrites = Map<string, Map<string, unknown>>;
|
|
|
27
28
|
/** One joined client. Presence is the wire truth; this is the runtime's bookkeeping. */
|
|
28
29
|
interface ClientEntry {
|
|
29
30
|
readonly clientId: string;
|
|
31
|
+
/**
|
|
32
|
+
* D44: this session is a scripted NPC, spawned by room code rather than dialled from outside.
|
|
33
|
+
* It changes exactly two things: the idle clock does not count it (a room full of NPCs and no
|
|
34
|
+
* players hibernates like an empty one), and metrics report it separately. Everything else —
|
|
35
|
+
* `maxClients`, validation, ownership, presence — treats it as the client session it is.
|
|
36
|
+
*/
|
|
37
|
+
readonly npc: boolean;
|
|
30
38
|
/**
|
|
31
39
|
* D25: the identity `ctx.playerId` reports. Set once at the first join and kept across
|
|
32
40
|
* reconnects, exactly like the client id it defaults to.
|
|
@@ -85,6 +93,8 @@ interface QueuedFrame {
|
|
|
85
93
|
}
|
|
86
94
|
/** What the core modules may do with the physics world (`core/physics.ts` implements it). */
|
|
87
95
|
interface PhysicsApi {
|
|
96
|
+
/** D45: which of the two blessed engines this room is running. */
|
|
97
|
+
readonly engineKind: 'rapier3d' | 'matter2d';
|
|
88
98
|
readonly timestep: number;
|
|
89
99
|
/** Creates bodies for new instances, destroys bodies whose instance is gone. */
|
|
90
100
|
reconcile(): void;
|
|
@@ -92,8 +102,22 @@ interface PhysicsApi {
|
|
|
92
102
|
/** Body → schema, through the tracked proxies. */
|
|
93
103
|
sync(): void;
|
|
94
104
|
bodyFor(collection: string, id: string): unknown;
|
|
105
|
+
/**
|
|
106
|
+
* D72: every body this runtime tracks, in the same collection-then-instance order `sync()`
|
|
107
|
+
* walks. Read-only and allocation-free: the pose history calls it once per tick to copy poses
|
|
108
|
+
* out, and the rewind scratch calls it to keep its doubles in step with the live world. It is
|
|
109
|
+
* the only thing outside the engine runtimes that sees a live body other than through
|
|
110
|
+
* `bodyFor`, and it never hands out the map itself.
|
|
111
|
+
*/
|
|
112
|
+
eachTrackedBody(fn: (collection: string, id: string, body: unknown) => void): void;
|
|
113
|
+
/** rapier3d only. */
|
|
95
114
|
readonly rapier: unknown;
|
|
115
|
+
/** rapier3d only. */
|
|
96
116
|
readonly world: unknown;
|
|
117
|
+
/** matter2d only: the `matter-js` namespace. */
|
|
118
|
+
readonly matter: unknown;
|
|
119
|
+
/** matter2d only: the live `Matter.Engine`. */
|
|
120
|
+
readonly matterEngine: unknown;
|
|
97
121
|
}
|
|
98
122
|
interface LoopApi {
|
|
99
123
|
start(): void;
|
|
@@ -133,7 +157,16 @@ interface RoomInternals {
|
|
|
133
157
|
stopped: boolean;
|
|
134
158
|
/** Runs a room handler; a throw is logged and counted, never rethrown. */
|
|
135
159
|
guard<T>(name: string, fn: () => T): T | undefined;
|
|
136
|
-
|
|
160
|
+
/** D41: record this tick on the authoritative timeline. A no-op while the recorder is unarmed. */
|
|
161
|
+
captureTimeline(): void;
|
|
162
|
+
/**
|
|
163
|
+
* D72: record this tick's body poses. A no-op — and, more to the point, a call that never
|
|
164
|
+
* reaches the engine runtime at all — in a room whose config declares no `physics.history`.
|
|
165
|
+
*/
|
|
166
|
+
captureHistory(): void;
|
|
167
|
+
/** D72: `room.rewind(tick, fn)`. Throws when the room declares no `physics.history`. */
|
|
168
|
+
rewind<T>(tick: number, fn: (past: RewindView) => T): T;
|
|
169
|
+
recordEvent(kind: 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'alarm' | 'bus' | 'error', clientId?: string, detail?: string): void;
|
|
137
170
|
/** `guard` that also reports whether the handler threw (the tick loop needs this). */
|
|
138
171
|
tryRun<T>(name: string, fn: () => T): GuardResult<T>;
|
|
139
172
|
log(level: LogLevel, ...args: unknown[]): void;
|
|
@@ -144,6 +177,13 @@ interface RoomInternals {
|
|
|
144
177
|
correctionFor(clientId: string): DirtySet | undefined;
|
|
145
178
|
/** Flushes the tracked dirty set: corrections first, then one `DELTA` per distinct view. */
|
|
146
179
|
flush(): void;
|
|
180
|
+
/**
|
|
181
|
+
* Schedules an event-mode flush after the current microtask chain settles; a no-op in tick
|
|
182
|
+
* mode (the next tick flushes anyway) and while one is already pending. This is the
|
|
183
|
+
* "own event, then flush" step the host-call seam runs after `completeHostCall`, exposed so a
|
|
184
|
+
* promise the room API rejects *locally* (bug #48) settles in the same scheduling class.
|
|
185
|
+
*/
|
|
186
|
+
scheduleContinuationFlush(): void;
|
|
147
187
|
/** Invalidates the cached `room.clients` array. */
|
|
148
188
|
invalidateClients(): void;
|
|
149
189
|
}
|
|
@@ -225,6 +265,106 @@ declare class Loop implements LoopApi {
|
|
|
225
265
|
after(ms: number, fn: () => void): void;
|
|
226
266
|
}
|
|
227
267
|
|
|
268
|
+
/**
|
|
269
|
+
* D45 part 1: the matter.js world inside the room.
|
|
270
|
+
*
|
|
271
|
+
* It sits in exactly the place `core/physics.ts` describes — reconcile, step, sync, between the
|
|
272
|
+
* `tick()` handler and the flush — and writes the same schema channels. What differs is everything
|
|
273
|
+
* underneath, and two differences are load-bearing enough to be worth stating here.
|
|
274
|
+
*
|
|
275
|
+
* ## There is no engine snapshot
|
|
276
|
+
*
|
|
277
|
+
* Rapier has `world.takeSnapshot()` / `restoreSnapshot()`, and a woken Rapier room is byte-for-byte
|
|
278
|
+
* the world that went to sleep, contacts included. matter.js has no equivalent: its world is plain
|
|
279
|
+
* JavaScript objects, and the honest options were to serialize the object graph ourselves or to
|
|
280
|
+
* rebuild the world and reapply per-body state. This module does the second. `setup` therefore runs
|
|
281
|
+
* on **every** matter2d wake, not only on a rebuild, and what is not restored is the solver's
|
|
282
|
+
* transient state: resting contacts, accumulated impulses, and the sleep timers behind them. A
|
|
283
|
+
* settled pile may re-settle with a small jolt. The measurement in `docs/m3-part5-report.md` says
|
|
284
|
+
* how big that jolt is on the fixture that ships with this.
|
|
285
|
+
*
|
|
286
|
+
* ## The units are matter.js's, not translated
|
|
287
|
+
*
|
|
288
|
+
* `gravity` goes into `engine.gravity` verbatim (matter's own default is `{ x: 0, y: 1 }` with
|
|
289
|
+
* `scale: 0.001`, and y is **down**), and `body.velocity` is matter's per-step displacement rather
|
|
290
|
+
* than a per-second velocity. Translating either would mean irtio inventing a unit system on top of
|
|
291
|
+
* an engine it promised to bless rather than abstract, and every matter.js tutorial would then be
|
|
292
|
+
* subtly wrong inside a room. The mapping onto the wire channels is in `@irtio/schema`
|
|
293
|
+
* (`channelOf2d`), and it is the only translation there is.
|
|
294
|
+
*/
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Loads `matter-js` once per process. Pure JavaScript, so there is no `init()` to await and no
|
|
298
|
+
* WASM instance to keep unique — but the load is still async (it is an `import()`), and handlers
|
|
299
|
+
* are synchronous, so it happens before the room is constructed exactly as Rapier's does.
|
|
300
|
+
*/
|
|
301
|
+
declare function initMatter(): Promise<MatterModule>;
|
|
302
|
+
declare function loadedMatter(): MatterModule | undefined;
|
|
303
|
+
/** Test seam: forget the loaded engine. */
|
|
304
|
+
declare function resetMatterForTests(): void;
|
|
305
|
+
/** One body's restorable state. Everything a rebuild needs and nothing the world-builder gives. */
|
|
306
|
+
interface MatterBodyRecord {
|
|
307
|
+
readonly collection: string;
|
|
308
|
+
readonly id: string;
|
|
309
|
+
readonly x: number;
|
|
310
|
+
readonly y: number;
|
|
311
|
+
readonly angle: number;
|
|
312
|
+
readonly vx: number;
|
|
313
|
+
readonly vy: number;
|
|
314
|
+
readonly angularVelocity: number;
|
|
315
|
+
readonly sleeping: boolean;
|
|
316
|
+
}
|
|
317
|
+
interface MatterSection {
|
|
318
|
+
readonly bodies: readonly MatterBodyRecord[];
|
|
319
|
+
}
|
|
320
|
+
declare function encodeMatterBodies(section: MatterSection): Uint8Array;
|
|
321
|
+
declare function decodeMatterBodies(bytes: Uint8Array): MatterSection;
|
|
322
|
+
interface MatterRuntimeOptions {
|
|
323
|
+
readonly restore?: MatterSection;
|
|
324
|
+
readonly defaultTimestep: number;
|
|
325
|
+
}
|
|
326
|
+
declare class MatterRuntime {
|
|
327
|
+
readonly engineKind: "matter2d";
|
|
328
|
+
/** rapier3d only; present so both runtimes satisfy one internal shape. */
|
|
329
|
+
readonly rapier: undefined;
|
|
330
|
+
readonly world: undefined;
|
|
331
|
+
readonly matter: MatterModule;
|
|
332
|
+
readonly engine: MatterEngine;
|
|
333
|
+
/** Alias under the name `PhysicsApi` uses, so `room.physics2d` reads through one field. */
|
|
334
|
+
get matterEngine(): MatterEngine;
|
|
335
|
+
/** `true` when the blob carried no world at all: the caller logs it. */
|
|
336
|
+
readonly rebuilt: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Always `true`. Unlike Rapier's, a matter2d world is never restored as a world — only as
|
|
339
|
+
* per-body state on a world the builder made — so `setup` has to run every time.
|
|
340
|
+
*/
|
|
341
|
+
readonly needsSetup = true;
|
|
342
|
+
private readonly core;
|
|
343
|
+
private readonly config;
|
|
344
|
+
private readonly collections;
|
|
345
|
+
private readonly bodies;
|
|
346
|
+
private readonly restore;
|
|
347
|
+
private readonly stepMs;
|
|
348
|
+
private readonly sleepSynced;
|
|
349
|
+
constructor(core: RoomInternals, matter: MatterModule, options: MatterRuntimeOptions);
|
|
350
|
+
get timestep(): number;
|
|
351
|
+
runSetup(room: Room): void;
|
|
352
|
+
free(): void;
|
|
353
|
+
bodyFor(collection: string, id: string): MatterBody | undefined;
|
|
354
|
+
private create;
|
|
355
|
+
/**
|
|
356
|
+
* D72: every tracked body, in the order `sync()` walks them. The matter half of the same
|
|
357
|
+
* read-only accessor `PhysicsRuntime` carries; see its comment for what calls it and when.
|
|
358
|
+
*/
|
|
359
|
+
eachTrackedBody(fn: (collection: string, id: string, body: MatterBody) => void): void;
|
|
360
|
+
private applyState;
|
|
361
|
+
private applyRecordToBody;
|
|
362
|
+
reconcile(): void;
|
|
363
|
+
step(): void;
|
|
364
|
+
sync(): void;
|
|
365
|
+
serialize(): MatterSection;
|
|
366
|
+
}
|
|
367
|
+
|
|
228
368
|
/**
|
|
229
369
|
* D22 part 1: the Rapier world inside the room.
|
|
230
370
|
*
|
|
@@ -281,6 +421,23 @@ interface PhysicsSection {
|
|
|
281
421
|
readonly bodies: readonly (readonly [string, string, number])[];
|
|
282
422
|
}
|
|
283
423
|
declare function encodePhysicsSection(section: PhysicsSection): Uint8Array;
|
|
424
|
+
/**
|
|
425
|
+
* D45: which engine wrote a v2 physics section.
|
|
426
|
+
*
|
|
427
|
+
* The discriminant is not a new leading byte, because a new leading byte would change every blob
|
|
428
|
+
* ever written. It is a **zero-length world**: a Rapier section starts with the varint length of
|
|
429
|
+
* `world.takeSnapshot()`, which is never zero, so `0` is a value no existing blob can hold. A
|
|
430
|
+
* matter2d section writes that zero, then a one-byte engine tag, then its own payload. Every
|
|
431
|
+
* pre-D45 blob is therefore byte-identical and reads as Rapier without a version bump, which is
|
|
432
|
+
* what the recommendation in the plan asked for and why format 3 was not needed.
|
|
433
|
+
*/
|
|
434
|
+
type PhysicsEngineTag = 'rapier3d' | 'matter2d';
|
|
435
|
+
/** Reads the engine out of an encoded section without decoding the rest of it. */
|
|
436
|
+
declare function physicsSectionEngine(bytes: Uint8Array): PhysicsEngineTag;
|
|
437
|
+
/** Wraps a matter2d body-state payload in the discriminated envelope. */
|
|
438
|
+
declare function encodeMatterSectionEnvelope(payload: Uint8Array): Uint8Array;
|
|
439
|
+
/** The payload inside a matter2d envelope. Throws if the section is a Rapier one. */
|
|
440
|
+
declare function decodeMatterSectionEnvelope(bytes: Uint8Array): Uint8Array;
|
|
284
441
|
declare function decodePhysicsSection(bytes: Uint8Array): PhysicsSection;
|
|
285
442
|
interface PhysicsRuntimeOptions {
|
|
286
443
|
/** From a v2 hibernation blob. Absent → a fresh world, and `setup` runs. */
|
|
@@ -289,10 +446,16 @@ interface PhysicsRuntimeOptions {
|
|
|
289
446
|
readonly defaultTimestep: number;
|
|
290
447
|
}
|
|
291
448
|
declare class PhysicsRuntime {
|
|
449
|
+
readonly engineKind: "rapier3d";
|
|
450
|
+
/** matter2d only; present so both runtimes satisfy one internal shape. */
|
|
451
|
+
readonly matter: undefined;
|
|
452
|
+
readonly matterEngine: undefined;
|
|
292
453
|
readonly rapier: RapierModule;
|
|
293
454
|
readonly world: RapierWorld;
|
|
294
455
|
/** `true` when the world was built from scratch and `setup` has to run. */
|
|
295
456
|
readonly rebuilt: boolean;
|
|
457
|
+
/** Rapier restores a whole world, contacts included, so `setup` runs only on a rebuild. */
|
|
458
|
+
get needsSetup(): boolean;
|
|
296
459
|
private readonly core;
|
|
297
460
|
private readonly config;
|
|
298
461
|
/** Physics-backed collections, in schema (name-sorted) order. */
|
|
@@ -315,6 +478,13 @@ declare class PhysicsRuntime {
|
|
|
315
478
|
/** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
|
|
316
479
|
bodyFor(collection: string, id: string): RapierRigidBody | undefined;
|
|
317
480
|
private create;
|
|
481
|
+
/**
|
|
482
|
+
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
483
|
+
* order). Read-only: it hands out the live bodies and nothing else, and the map stays private.
|
|
484
|
+
* The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
|
|
485
|
+
* that declares no `physics.history` never calls it at all.
|
|
486
|
+
*/
|
|
487
|
+
eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody) => void): void;
|
|
318
488
|
private applyRecordToBody;
|
|
319
489
|
/**
|
|
320
490
|
* Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
|
|
@@ -351,7 +521,7 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
351
521
|
readonly clients: Map<string, ClientEntry>;
|
|
352
522
|
readonly loop: Loop;
|
|
353
523
|
/** D22: the Rapier world, or `undefined` in a room whose config declares no physics. */
|
|
354
|
-
readonly physics: PhysicsRuntime | undefined;
|
|
524
|
+
readonly physics: PhysicsRuntime | MatterRuntime | undefined;
|
|
355
525
|
readonly stats: RoomStats;
|
|
356
526
|
tick: number;
|
|
357
527
|
stopped: boolean;
|
|
@@ -362,6 +532,24 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
362
532
|
* into a timeout in an unrelated assertion ten seconds later. Unset in production.
|
|
363
533
|
*/
|
|
364
534
|
onHandlerError: ((name: string, err: unknown) => void) | undefined;
|
|
535
|
+
/**
|
|
536
|
+
* D41: the recorded authoritative timeline. Armed by `startRecording()` and off otherwise, so a
|
|
537
|
+
* room nobody asked to record pays nothing. A room that was asked captures its own state at the
|
|
538
|
+
* end of every tick, which is the only moment in a tick where that state is settled.
|
|
539
|
+
*/
|
|
540
|
+
private recorder;
|
|
541
|
+
/**
|
|
542
|
+
* D65: the bandwidth ledger, present only when `RoomCoreOptions.profile` asked for one. Every
|
|
543
|
+
* cost the profiler has is behind this `undefined`.
|
|
544
|
+
*/
|
|
545
|
+
private readonly ledger;
|
|
546
|
+
/**
|
|
547
|
+
* D72: the pose history and the rewind scratch, present only when the room's physics config
|
|
548
|
+
* declares `history`. Every cost this lane has is behind this `undefined`, and it is
|
|
549
|
+
* deliberately not in the hibernation blob: a woken room starts with an empty buffer and fills
|
|
550
|
+
* it again over its next `history` ticks.
|
|
551
|
+
*/
|
|
552
|
+
private readonly rewindState;
|
|
365
553
|
private readonly seed;
|
|
366
554
|
private readonly api;
|
|
367
555
|
private readonly internals;
|
|
@@ -369,6 +557,26 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
369
557
|
/** One pending continuation flush at a time; concurrent completions coalesce into it. */
|
|
370
558
|
private continuationFlushPending;
|
|
371
559
|
constructor(definition: RoomDefinition<S>, host: RoomHost, options: RoomCoreOptions);
|
|
560
|
+
private subscribeDeclaredChannels;
|
|
561
|
+
private get busConfig();
|
|
562
|
+
/**
|
|
563
|
+
* D59: one published message arriving on a channel this room is subscribed to.
|
|
564
|
+
*
|
|
565
|
+
* Same scheduling class as an alarm or an RPC — a discrete event between ticks — so a tick-mode
|
|
566
|
+
* room never sees a `tick` run half-delivered. `from` is supervisor-stamped, so the handler may
|
|
567
|
+
* trust it as far as it trusts its own project.
|
|
568
|
+
*/
|
|
569
|
+
deliverBusEvent(channel: string, from: string, payload: string): void;
|
|
570
|
+
/**
|
|
571
|
+
* D59: one directed `room.bus.send` arriving. At-least-once, so this can run twice for one send;
|
|
572
|
+
* that is the receiver's problem to be idempotent about and the docs say so.
|
|
573
|
+
*
|
|
574
|
+
* A throw propagates through `guard` and counts toward the crash threshold exactly as any other
|
|
575
|
+
* handler throw does. That is deliberate, and it is why the supervisor bounds redelivery: the
|
|
576
|
+
* two behaviours together would otherwise let one poisonous message close a room on every wake,
|
|
577
|
+
* forever.
|
|
578
|
+
*/
|
|
579
|
+
deliverBusMessage(from: string, payload: string): void;
|
|
372
580
|
/**
|
|
373
581
|
* D22: builds the world, or returns `undefined` for a room with no `physics:` config.
|
|
374
582
|
*
|
|
@@ -381,6 +589,13 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
381
589
|
* and is lost; a stack of boxes may settle again with a small visible jolt.
|
|
382
590
|
*/
|
|
383
591
|
private buildPhysics;
|
|
592
|
+
/**
|
|
593
|
+
* D45: the matter2d half. It differs from Rapier's in one structural way — there is no engine
|
|
594
|
+
* snapshot to restore, so the world is always **built** and per-body state is reapplied on top
|
|
595
|
+
* of it. `setup` therefore runs on every wake, and only the "there was no world at all" case is
|
|
596
|
+
* worth logging.
|
|
597
|
+
*/
|
|
598
|
+
private buildMatter;
|
|
384
599
|
/** Convenience for hosts: `RoomCore.restore(def, bytes, host, opts)`. */
|
|
385
600
|
static restore<S2 extends AnySchema>(definition: RoomDefinition<S2>, bytes: Uint8Array, host: RoomHost, options: Omit<RoomCoreOptions, 'restoreFrom'>): RoomCore<S2>;
|
|
386
601
|
get schema(): S;
|
|
@@ -390,6 +605,27 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
390
605
|
tryRun<T>(name: string, fn: () => T): GuardResult<T>;
|
|
391
606
|
private readonly events;
|
|
392
607
|
recordEvent(kind: RoomEventKind, clientId?: string, detail?: string): void;
|
|
608
|
+
/**
|
|
609
|
+
* D41: begin (or restart) recording the authoritative timeline. Calling it again clears what
|
|
610
|
+
* was recorded, so two scenario runs against one long-lived dev server do not read each
|
|
611
|
+
* other's ticks.
|
|
612
|
+
*/
|
|
613
|
+
startRecording(options?: TimelineRecorderOptions): void;
|
|
614
|
+
/** D41: what has been recorded so far, or `undefined` when nobody armed the recorder. */
|
|
615
|
+
recording(): TimelineDump | undefined;
|
|
616
|
+
/** D41: called at the end of every tick (and every event-mode flush). No-op when unarmed. */
|
|
617
|
+
captureTimeline(): void;
|
|
618
|
+
/**
|
|
619
|
+
* D72: record this tick's body poses, right after `physics.sync()` — the poses the clients are
|
|
620
|
+
* about to be told about, under the tick number they will be told it under, which is the tick a
|
|
621
|
+
* client later stamps its `CALL` with.
|
|
622
|
+
*/
|
|
623
|
+
captureHistory(): void;
|
|
624
|
+
/**
|
|
625
|
+
* D72: `room.rewind(tick, fn)`. The live world is not touched and nothing is re-simulated; `fn`
|
|
626
|
+
* queries a scratch world holding every tracked body at its pose at `tick`.
|
|
627
|
+
*/
|
|
628
|
+
rewind<T>(tick: number, fn: (past: RewindView) => T): T;
|
|
393
629
|
/** Live JSON view of the room for the dev page / supervisor admin API. */
|
|
394
630
|
inspect(): RoomInspection;
|
|
395
631
|
guard<T>(name: string, fn: () => T): T | undefined;
|
|
@@ -434,8 +670,12 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
434
670
|
* Draining a bounded number of turns first covers the chains rooms actually write, coalesces
|
|
435
671
|
* concurrent completions into one flush, and — unlike a `setTimeout(0)` — keeps working under
|
|
436
672
|
* the harness's synchronous fake clock, where a macrotask would fire *before* the microtasks.
|
|
673
|
+
*
|
|
674
|
+
* Public on `RoomInternals` (bug #48) so the room API's local rejections — a float score, an
|
|
675
|
+
* empty bus target, a call to a client that is not connected — flush the state their `.catch()`
|
|
676
|
+
* writes, instead of leaving it for whatever frame happens to arrive next.
|
|
437
677
|
*/
|
|
438
|
-
|
|
678
|
+
scheduleContinuationFlush(): void;
|
|
439
679
|
/**
|
|
440
680
|
* D26: one durable alarm firing. Same scheduling class as an RPC — a discrete event between
|
|
441
681
|
* ticks — so a tick-mode room never sees a `tick` run half-alarmed, and a handler that re-arms
|
|
@@ -444,7 +684,19 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
444
684
|
fireAlarm(name: string): void;
|
|
445
685
|
correctionFor(clientId: string): DirtySet | undefined;
|
|
446
686
|
invalidateClients(): void;
|
|
447
|
-
|
|
687
|
+
/**
|
|
688
|
+
* D65: `hint` is profiling context and nothing else — the payload object a per-view frame was
|
|
689
|
+
* built from (so one encode is walked once and replayed per recipient) and the AOI ids whose
|
|
690
|
+
* ops are visibility churn. Ignored entirely when this room is not profiling, which is why it
|
|
691
|
+
* is an optional argument rather than a second method.
|
|
692
|
+
*/
|
|
693
|
+
send(clientId: string, frame: Uint8Array, hint?: AttributeOptions): void;
|
|
694
|
+
/**
|
|
695
|
+
* D65: the ledger so far. The room's own view, so it counts what `send()` sent and what
|
|
696
|
+
* `receive()` accepted, plus the join snapshots this room handed the host to wrap in a
|
|
697
|
+
* `WELCOME` (the host builds that frame, so the room never sees it — see the docs page).
|
|
698
|
+
*/
|
|
699
|
+
profile(): ProfileSnapshot | undefined;
|
|
448
700
|
private badFrame;
|
|
449
701
|
receive(clientId: string, frame: Uint8Array): void;
|
|
450
702
|
/**
|
|
@@ -455,4 +707,4 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
455
707
|
flush(): void;
|
|
456
708
|
}
|
|
457
709
|
|
|
458
|
-
export { CRASH_AFTER_THROWS as C, MAX_CATCHUP as M, type
|
|
710
|
+
export { CRASH_AFTER_THROWS as C, MAX_CATCHUP as M, type PhysicsEngineTag as P, RoomCore as R, type MatterBodyRecord as a, type MatterSection as b, Mulberry32 as c, type PhysicsSection as d, decodeMatterBodies as e, decodeMatterSectionEnvelope as f, decodePhysicsSection as g, encodeMatterBodies as h, encodeMatterSectionEnvelope as i, encodePhysicsSection as j, initMatter as k, initPhysics as l, loadedMatter as m, loadedPhysics as n, resetPhysicsForTests as o, physicsSectionEngine as p, resetMatterForTests as r };
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { R as RoomCore } from '../room-
|
|
2
|
-
export {
|
|
1
|
+
import { R as RoomCore } from '../room-CoQDczh2.js';
|
|
2
|
+
export { k as initMatter, l as initPhysics } from '../room-CoQDczh2.js';
|
|
3
3
|
import { ErrorCodeName, FrameType } from '@irtio/protocol';
|
|
4
|
-
import {
|
|
4
|
+
import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
5
|
+
import { i as RoomHost, L as LogLevel, R as RoomCoreApi, b as HostCall, c as HostCallResult, k as RoomStats } from '../contract-DwxqjeWP.js';
|
|
5
6
|
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
6
|
-
import { LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The harness clock: a deterministic fake `now()` plus an ordered timer queue. `RoomCore` only
|
|
@@ -90,6 +90,23 @@ declare class HarnessHost implements RoomHost {
|
|
|
90
90
|
hostCallDelayMs: number;
|
|
91
91
|
/** The in-memory player KV, keyed the way the real table's composite primary key is. */
|
|
92
92
|
readonly kv: Map<string, string>;
|
|
93
|
+
/** D53: `"<board> <playerId>"` -> the best score submitted so far. */
|
|
94
|
+
readonly scores: Map<string, number>;
|
|
95
|
+
/** D63: every `room.ratings.report` the room asked for, in order. Recorded, never applied. */
|
|
96
|
+
readonly ratingReports: {
|
|
97
|
+
queue: string;
|
|
98
|
+
results: {
|
|
99
|
+
playerId: string;
|
|
100
|
+
place: number;
|
|
101
|
+
}[];
|
|
102
|
+
}[];
|
|
103
|
+
/** D63: every `room.ratings.set`, in order. */
|
|
104
|
+
readonly ratingSets: {
|
|
105
|
+
queue: string;
|
|
106
|
+
playerId: string;
|
|
107
|
+
rating: number;
|
|
108
|
+
deviation?: number;
|
|
109
|
+
}[];
|
|
93
110
|
/** Save generations this host minted: `saveId` -> the bytes it was handed. */
|
|
94
111
|
readonly saves: Map<string, Uint8Array<ArrayBufferLike>>;
|
|
95
112
|
/** What `room.save()` serializes. The harness sets it to `() => core.snapshot()` — the bytes
|
|
@@ -121,6 +138,36 @@ declare class HarnessHost implements RoomHost {
|
|
|
121
138
|
crashed(reason: string): void;
|
|
122
139
|
hostCall(reqId: number, call: HostCall): void;
|
|
123
140
|
private runHostCall;
|
|
141
|
+
/**
|
|
142
|
+
* D44: the in-process harness has no supervisor, no sockets and therefore no loopback session
|
|
143
|
+
* to open. Recording the request rather than faking a session is the honest option: a test that
|
|
144
|
+
* wants to see an NPC *play* needs a real tenant (`packages/supervisor/test/npc.test.ts`), and
|
|
145
|
+
* one that wants to see a room ask for one reads these.
|
|
146
|
+
*/
|
|
147
|
+
readonly spawnedNpcs: {
|
|
148
|
+
clientId: string;
|
|
149
|
+
config: NpcConfig;
|
|
150
|
+
}[];
|
|
151
|
+
readonly despawnedNpcs: string[];
|
|
152
|
+
spawnNpc(clientId: string, config: NpcConfig): void;
|
|
153
|
+
despawnNpc(clientId: string): void;
|
|
154
|
+
/** Every `room.bus.send` the room asked for, in order. */
|
|
155
|
+
readonly busSends: {
|
|
156
|
+
roomId: string;
|
|
157
|
+
payload: string;
|
|
158
|
+
}[];
|
|
159
|
+
/** Every `room.bus.publish`, in order. */
|
|
160
|
+
readonly busPublishes: {
|
|
161
|
+
channel: string;
|
|
162
|
+
payload: string;
|
|
163
|
+
}[];
|
|
164
|
+
/** The channels this room is currently subscribed to, in subscription order. */
|
|
165
|
+
readonly busSubscriptions: Set<string>;
|
|
166
|
+
/** D63-e: the harness has no room list to report on, so it records the value and stops. */
|
|
167
|
+
backfillOpen: boolean | undefined;
|
|
168
|
+
setBackfill(open: boolean): void;
|
|
169
|
+
busPublish(channel: string, payload: string): void;
|
|
170
|
+
busSubscribe(channel: string, subscribed: boolean): void;
|
|
124
171
|
setAlarm(name: string, atMs: number | undefined): void;
|
|
125
172
|
/**
|
|
126
173
|
* Fires every alarm due at or before `now`, in **name order** (D34 determinism), removing each
|
package/dist/test/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RoomCore,
|
|
3
3
|
createVisibilityPolicy,
|
|
4
|
+
initMatter,
|
|
4
5
|
initPhysics,
|
|
5
6
|
visibleNames
|
|
6
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-EXPFVRD4.js";
|
|
7
8
|
|
|
8
9
|
// src/test/clock.ts
|
|
9
10
|
var FakeClock = class {
|
|
@@ -79,6 +80,12 @@ var HarnessHost = class {
|
|
|
79
80
|
hostCallDelayMs = 0;
|
|
80
81
|
/** The in-memory player KV, keyed the way the real table's composite primary key is. */
|
|
81
82
|
kv = /* @__PURE__ */ new Map();
|
|
83
|
+
/** D53: `"<board> <playerId>"` -> the best score submitted so far. */
|
|
84
|
+
scores = /* @__PURE__ */ new Map();
|
|
85
|
+
/** D63: every `room.ratings.report` the room asked for, in order. Recorded, never applied. */
|
|
86
|
+
ratingReports = [];
|
|
87
|
+
/** D63: every `room.ratings.set`, in order. */
|
|
88
|
+
ratingSets = [];
|
|
82
89
|
/** Save generations this host minted: `saveId` -> the bytes it was handed. */
|
|
83
90
|
saves = /* @__PURE__ */ new Map();
|
|
84
91
|
/** What `room.save()` serializes. The harness sets it to `() => core.snapshot()` — the bytes
|
|
@@ -169,11 +176,72 @@ var HarnessHost = class {
|
|
|
169
176
|
case "kvDelete":
|
|
170
177
|
this.kv.delete(kvKey(call.playerId, call.key));
|
|
171
178
|
return { ok: true };
|
|
179
|
+
case "busSend": {
|
|
180
|
+
this.busSends.push({ roomId: call.roomId, payload: call.payload });
|
|
181
|
+
return { ok: true };
|
|
182
|
+
}
|
|
183
|
+
case "ratingReport": {
|
|
184
|
+
this.ratingReports.push({
|
|
185
|
+
queue: call.queue,
|
|
186
|
+
results: call.results.map((r) => ({ ...r }))
|
|
187
|
+
});
|
|
188
|
+
return { ok: true };
|
|
189
|
+
}
|
|
190
|
+
case "ratingSet": {
|
|
191
|
+
this.ratingSets.push({
|
|
192
|
+
queue: call.queue,
|
|
193
|
+
playerId: call.playerId,
|
|
194
|
+
rating: call.rating,
|
|
195
|
+
...call.deviation !== void 0 ? { deviation: call.deviation } : {}
|
|
196
|
+
});
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
case "lbSubmit": {
|
|
200
|
+
const key = call.bucket === void 0 ? `${call.board} ${call.playerId}` : `${call.board} ${call.bucket} ${call.playerId}`;
|
|
201
|
+
const existing = this.scores.get(key);
|
|
202
|
+
if (existing === void 0 || call.score > existing) this.scores.set(key, call.score);
|
|
203
|
+
return { ok: true };
|
|
204
|
+
}
|
|
172
205
|
}
|
|
173
206
|
}
|
|
174
207
|
// -------------------------------------------------------------------------
|
|
175
208
|
// Week 12: durable alarms (D26)
|
|
176
209
|
// -------------------------------------------------------------------------
|
|
210
|
+
/**
|
|
211
|
+
* D44: the in-process harness has no supervisor, no sockets and therefore no loopback session
|
|
212
|
+
* to open. Recording the request rather than faking a session is the honest option: a test that
|
|
213
|
+
* wants to see an NPC *play* needs a real tenant (`packages/supervisor/test/npc.test.ts`), and
|
|
214
|
+
* one that wants to see a room ask for one reads these.
|
|
215
|
+
*/
|
|
216
|
+
spawnedNpcs = [];
|
|
217
|
+
despawnedNpcs = [];
|
|
218
|
+
spawnNpc(clientId, config) {
|
|
219
|
+
this.spawnedNpcs.push({ clientId, config });
|
|
220
|
+
}
|
|
221
|
+
despawnNpc(clientId) {
|
|
222
|
+
this.despawnedNpcs.push(clientId);
|
|
223
|
+
}
|
|
224
|
+
// -------------------------------------------------------------------------
|
|
225
|
+
// D59: the bus
|
|
226
|
+
// -------------------------------------------------------------------------
|
|
227
|
+
/** Every `room.bus.send` the room asked for, in order. */
|
|
228
|
+
busSends = [];
|
|
229
|
+
/** Every `room.bus.publish`, in order. */
|
|
230
|
+
busPublishes = [];
|
|
231
|
+
/** The channels this room is currently subscribed to, in subscription order. */
|
|
232
|
+
busSubscriptions = /* @__PURE__ */ new Set();
|
|
233
|
+
/** D63-e: the harness has no room list to report on, so it records the value and stops. */
|
|
234
|
+
backfillOpen;
|
|
235
|
+
setBackfill(open) {
|
|
236
|
+
this.backfillOpen = open;
|
|
237
|
+
}
|
|
238
|
+
busPublish(channel, payload) {
|
|
239
|
+
this.busPublishes.push({ channel, payload });
|
|
240
|
+
}
|
|
241
|
+
busSubscribe(channel, subscribed) {
|
|
242
|
+
if (subscribed) this.busSubscriptions.add(channel);
|
|
243
|
+
else this.busSubscriptions.delete(channel);
|
|
244
|
+
}
|
|
177
245
|
setAlarm(name, atMs) {
|
|
178
246
|
if (atMs === void 0) this.armedAlarms.delete(name);
|
|
179
247
|
else this.armedAlarms.set(name, atMs);
|
|
@@ -905,5 +973,6 @@ export {
|
|
|
905
973
|
HarnessHost,
|
|
906
974
|
createRoomHarness,
|
|
907
975
|
frameTypeName,
|
|
976
|
+
initMatter,
|
|
908
977
|
initPhysics
|
|
909
978
|
};
|