@irtio/runtime 0.10.0 → 0.11.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-MTBMGUGG.js → chunk-ACC3SDIO.js} +1 -1
- package/dist/{chunk-KGZEQTJR.js → chunk-GYPUNTQN.js} +1001 -94
- package/dist/{contract-C5aqs49-.d.ts → contract-Hk4SkDtW.d.ts} +101 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +12 -2
- package/dist/{room-Db3iPBKM.d.ts → room-0FZBGyke.d.ts} +9 -1
- package/dist/test/index.d.ts +26 -38
- package/dist/test/index.js +60 -4
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +2 -2
- package/package.json +4 -4
|
@@ -100,6 +100,68 @@ interface RoomInspection {
|
|
|
100
100
|
readonly recent: readonly RoomEvent[];
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* M6 lane K (D77 / O28): the write core's caps, and the named refusals that enforce them.
|
|
105
|
+
*
|
|
106
|
+
* ## Why these are here and not in the relay host
|
|
107
|
+
*
|
|
108
|
+
* The relay host is the reason they exist — it serves many tenants from one process, so a room
|
|
109
|
+
* that grows without limit is a room taking a box down with it — but the checks belong on the
|
|
110
|
+
* write path, which is the runtime's. Putting them here means a client `add` cannot get past them
|
|
111
|
+
* whichever host is running the room, and it means a test can drive them without a socket.
|
|
112
|
+
*
|
|
113
|
+
* ## They are walls, not budgets
|
|
114
|
+
*
|
|
115
|
+
* A write that would cross one is refused. The room carries on; every other client is unaffected;
|
|
116
|
+
* nothing is evicted, degraded, truncated or silently dropped. The refusal goes back to the writer
|
|
117
|
+
* as a **non-fatal** `ERROR` naming the cap it met ({@link ErrorCode.E_STATE_CAP},
|
|
118
|
+
* `E_RECORD_CAP`, `E_WRITE_RATE`), so a developer can tell a sizing decision from a bug without
|
|
119
|
+
* reading a log they may not have.
|
|
120
|
+
*
|
|
121
|
+
* ## What a room that did not ask for them pays
|
|
122
|
+
*
|
|
123
|
+
* Nothing. `stateBytes` and `writesPerSecond` are unset unless a host sets them, and the host that
|
|
124
|
+
* sets them is the relay host. A coded room in its own VM has its own memory ceiling and its own
|
|
125
|
+
* tick budget and does not need a second copy of either. The record cap is the exception and it is
|
|
126
|
+
* on by default, because client `add` is a new surface: no room written before this could regress
|
|
127
|
+
* on a limit that only applies to a thing it could not do.
|
|
128
|
+
*
|
|
129
|
+
* ## How state size is measured, and the error that leaves
|
|
130
|
+
*
|
|
131
|
+
* Encoding 512 KB of state on every write to find out how big it is would cost more than the write
|
|
132
|
+
* loop it guards. So: an exact measure (`encodeSnapshot(...).length`) is taken at most once a tick,
|
|
133
|
+
* and only once the running figure is over half the cap **or has gone `STALE_MEASURE_TICKS`
|
|
134
|
+
* without one**; between measures the figure is grown by a **conservative upper bound** of what
|
|
135
|
+
* each accepted write could add (a record's maximum encoded size, a string field's maximum encoded
|
|
136
|
+
* size).
|
|
137
|
+
*
|
|
138
|
+
* The invariant that buys, stated exactly: the figure is an upper bound on the truth *for
|
|
139
|
+
* everything it can see*, which is client writes, and it is re-anchored to the truth at least
|
|
140
|
+
* every `STALE_MEASURE_TICKS` ticks. It is emphatically **not** an upper bound between
|
|
141
|
+
* re-anchorings for what it cannot see — a `perPlayer` record added on join, an ownership change,
|
|
142
|
+
* a coded room's own handler writing state — and the staleness bound is what closes that: a leak
|
|
143
|
+
* the meter is blind to can outrun the figure for ten seconds at 30 Hz and no longer. Above the
|
|
144
|
+
* truth it can be by one tick of accepted client writes, which is what a slightly early refusal
|
|
145
|
+
* costs. A room under half the cap pays one measure per stale window and nothing else.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/** O28's numbers, as the relay host sets them. One place, so lane J can move them once. */
|
|
149
|
+
declare const RELAY_STATE_BYTES: number;
|
|
150
|
+
declare const RELAY_RECORDS_PER_COLLECTION = 1024;
|
|
151
|
+
declare const RELAY_WRITES_PER_SECOND = 35;
|
|
152
|
+
/** The record cap every room gets, because client `add` is a surface no old room had. */
|
|
153
|
+
declare const DEFAULT_RECORDS_PER_COLLECTION = 1024;
|
|
154
|
+
interface WriteCaps {
|
|
155
|
+
/** Encoded state bytes. Unset (the default) means no wall: the host has its own memory ceiling. */
|
|
156
|
+
readonly stateBytes?: number;
|
|
157
|
+
/** Records one entity collection may hold. Defaults to {@link DEFAULT_RECORDS_PER_COLLECTION}. */
|
|
158
|
+
readonly recordsPerCollection?: number;
|
|
159
|
+
/** `WRITE` frames one client may send a second. Unset means no bucket. */
|
|
160
|
+
readonly writesPerSecond?: number;
|
|
161
|
+
}
|
|
162
|
+
/** What the relay host passes. Named so a test can assert the numbers rather than repeat them. */
|
|
163
|
+
declare const RELAY_WRITE_CAPS: WriteCaps;
|
|
164
|
+
|
|
103
165
|
type LogLevel = 'info' | 'warn' | 'error';
|
|
104
166
|
/**
|
|
105
167
|
* Week 12: host-side async work a room asked for — a save generation (D24) or a player-KV
|
|
@@ -194,6 +256,21 @@ type HostCall = {
|
|
|
194
256
|
readonly playerId: string;
|
|
195
257
|
readonly rating: number;
|
|
196
258
|
readonly deviation?: number;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* D81: `room.replay.clip(seconds)` — the assembled clip blob, on its way to storage.
|
|
262
|
+
*
|
|
263
|
+
* The one crossing this lane adds, and a host call for the reason every other one here is: the
|
|
264
|
+
* room has no network and no store. It is also the only host call that carries bulk bytes, so
|
|
265
|
+
* it is the only one with a size wall on the far side (`CLIP_MAX_BYTES`, checked by the
|
|
266
|
+
* supervisor before anything is written) — the ring's own bound already implies a smaller
|
|
267
|
+
* number, but that bound lives on the side of the seam the tenant's own code owns.
|
|
268
|
+
*
|
|
269
|
+
* The result's `value` is the minted clip id.
|
|
270
|
+
*/
|
|
271
|
+
| {
|
|
272
|
+
readonly kind: 'clip';
|
|
273
|
+
readonly bytes: Uint8Array;
|
|
197
274
|
};
|
|
198
275
|
/**
|
|
199
276
|
* What a `HostCall` produced. `value` carries the save id for `save` and the stored string for
|
|
@@ -318,6 +395,21 @@ interface RoomCoreOptions {
|
|
|
318
395
|
* `testRoom({ profile: true })` are what turn it on.
|
|
319
396
|
*/
|
|
320
397
|
readonly profile?: boolean;
|
|
398
|
+
/**
|
|
399
|
+
* D77 / O28: the write core's caps — encoded state bytes, records per collection, writes per
|
|
400
|
+
* client per second. A host that sets none gets the record cap and nothing else, which is what
|
|
401
|
+
* every coded room in its own VM wants: it already has a memory ceiling and a tick budget. The
|
|
402
|
+
* relay host sets all three, because it serves many tenants from one process.
|
|
403
|
+
*/
|
|
404
|
+
readonly caps?: WriteCaps;
|
|
405
|
+
/**
|
|
406
|
+
* D77: the wall clock, for hold deadlines and nothing else.
|
|
407
|
+
*
|
|
408
|
+
* A hold has to keep running while a room is hibernated, and `RoomHost.now()` is monotonic
|
|
409
|
+
* within one process, so it cannot answer "is this five-minute hold over?" for a room that slept
|
|
410
|
+
* through it. Defaults to `Date.now`; injectable so a test can move a long hold without waiting.
|
|
411
|
+
*/
|
|
412
|
+
readonly wallNow?: () => number;
|
|
321
413
|
}
|
|
322
414
|
interface JoinOptions {
|
|
323
415
|
/** Requested role; unknown/missing → `roles[0]` (logged) or `''` when the schema has no roles. */
|
|
@@ -443,6 +535,14 @@ interface RoomCoreApi<S extends AnySchema = AnySchema> {
|
|
|
443
535
|
deliverBusEvent(channel: string, from: string, payload: string): void;
|
|
444
536
|
/** D59: deliver one directed `room.bus.send`. At-least-once, so this may repeat a message. */
|
|
445
537
|
deliverBusMessage(from: string, payload: string): void;
|
|
538
|
+
/**
|
|
539
|
+
* D77: the encoded size of this room's state, or `undefined` when no host asked for the state
|
|
540
|
+
* cap and nothing is metering it. What the room row and `irtio rooms` report, and the reason a
|
|
541
|
+
* developer sees the number climb long before the wall.
|
|
542
|
+
*/
|
|
543
|
+
stateBytes(): number | undefined;
|
|
544
|
+
/** D77: how many records this room is holding for players who have left. */
|
|
545
|
+
holdCount(): number;
|
|
446
546
|
}
|
|
447
547
|
|
|
448
|
-
export {
|
|
548
|
+
export { DEFAULT_RECORDS_PER_COLLECTION as D, EVENT_RING_SIZE as E, HOST_CALL_TIMEOUT_MS as H, type JoinOptions as J, type LogLevel as L, RELAY_RECORDS_PER_COLLECTION as R, type TimelineDump as T, type WriteCaps as W, DEFAULT_TIMELINE_MAX_RECORDS as a, DEFAULT_TIMELINE_MAX_TICKS as b, type HostCall as c, type HostCallResult as d, type JoinResult as e, RELAY_STATE_BYTES as f, RELAY_WRITES_PER_SECOND as g, RELAY_WRITE_CAPS as h, type RoomCoreApi as i, type RoomCoreOptions as j, type RoomEvent as k, type RoomEventKind as l, RoomFullError as m, type RoomHost as n, type RoomInspection as o, type RoomStats as p, type TimelineFrame as q, TimelineRecorder as r, type TimelineRecorderOptions as s, inspectState as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { D as
|
|
2
|
-
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-
|
|
1
|
+
export { D as DEFAULT_RECORDS_PER_COLLECTION, a as DEFAULT_TIMELINE_MAX_RECORDS, b as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, c as HostCall, d as HostCallResult, J as JoinOptions, e as JoinResult, L as LogLevel, R as RELAY_RECORDS_PER_COLLECTION, f as RELAY_STATE_BYTES, g as RELAY_WRITES_PER_SECOND, h as RELAY_WRITE_CAPS, i as RoomCoreApi, j as RoomCoreOptions, k as RoomEvent, l as RoomEventKind, m as RoomFullError, n as RoomHost, o as RoomInspection, p as RoomStats, T as TimelineDump, q as TimelineFrame, r as TimelineRecorder, s as TimelineRecorderOptions, W as WriteCaps, t as inspectState } from './contract-Hk4SkDtW.js';
|
|
2
|
+
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-0FZBGyke.js';
|
|
3
3
|
import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
|
|
4
4
|
import '@irtio/protocol';
|
|
5
5
|
import '@irtio/server';
|
|
@@ -118,11 +118,13 @@ interface ParsedSnapshot extends SnapshotHeader {
|
|
|
118
118
|
readonly version: number;
|
|
119
119
|
/** The encoded physics section, or `undefined` for a v1 blob / a room with no world. */
|
|
120
120
|
readonly physics: Uint8Array | undefined;
|
|
121
|
+
/** M6 lane K (D77): the hold deadlines section, or `undefined` when the room held nothing. */
|
|
122
|
+
readonly holds: Uint8Array | undefined;
|
|
121
123
|
/** The codec snapshot, encoded under `withBuiltins(schema)`. */
|
|
122
124
|
readonly snapshot: Uint8Array;
|
|
123
125
|
}
|
|
124
126
|
declare function parseHibernationBlob(bytes: Uint8Array): ParsedSnapshot;
|
|
125
|
-
declare function writeHibernationBlob(header: SnapshotHeader, snapshot: Uint8Array, physics?: Uint8Array): Uint8Array;
|
|
127
|
+
declare function writeHibernationBlob(header: SnapshotHeader, snapshot: Uint8Array, physics?: Uint8Array, holds?: Uint8Array): Uint8Array;
|
|
126
128
|
/** What {@link decodeSave} recovers from a room save's bytes. */
|
|
127
129
|
interface DecodedSave extends SnapshotHeader {
|
|
128
130
|
/** The blob's own format version byte: 1 (no physics section) or 2. */
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,10 @@ import {
|
|
|
2
2
|
fromMigrationState,
|
|
3
3
|
migrateSnapshot,
|
|
4
4
|
toMigrationState
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ACC3SDIO.js";
|
|
6
6
|
import {
|
|
7
7
|
CRASH_AFTER_THROWS,
|
|
8
|
+
DEFAULT_RECORDS_PER_COLLECTION,
|
|
8
9
|
DEFAULT_TIMELINE_MAX_RECORDS,
|
|
9
10
|
DEFAULT_TIMELINE_MAX_TICKS,
|
|
10
11
|
EVENT_RING_SIZE,
|
|
@@ -12,6 +13,10 @@ import {
|
|
|
12
13
|
MAX_CATCHUP,
|
|
13
14
|
Mulberry32,
|
|
14
15
|
READABLE_SNAPSHOT_VERSIONS,
|
|
16
|
+
RELAY_RECORDS_PER_COLLECTION,
|
|
17
|
+
RELAY_STATE_BYTES,
|
|
18
|
+
RELAY_WRITES_PER_SECOND,
|
|
19
|
+
RELAY_WRITE_CAPS,
|
|
15
20
|
RPC_TIMEOUT_MS,
|
|
16
21
|
RoomCore,
|
|
17
22
|
RoomFullError,
|
|
@@ -47,9 +52,10 @@ import {
|
|
|
47
52
|
visibleNames,
|
|
48
53
|
visibleTo,
|
|
49
54
|
writeHibernationBlob
|
|
50
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-GYPUNTQN.js";
|
|
51
56
|
export {
|
|
52
57
|
CRASH_AFTER_THROWS,
|
|
58
|
+
DEFAULT_RECORDS_PER_COLLECTION,
|
|
53
59
|
DEFAULT_TIMELINE_MAX_RECORDS,
|
|
54
60
|
DEFAULT_TIMELINE_MAX_TICKS,
|
|
55
61
|
EVENT_RING_SIZE,
|
|
@@ -57,6 +63,10 @@ export {
|
|
|
57
63
|
MAX_CATCHUP,
|
|
58
64
|
Mulberry32,
|
|
59
65
|
READABLE_SNAPSHOT_VERSIONS,
|
|
66
|
+
RELAY_RECORDS_PER_COLLECTION,
|
|
67
|
+
RELAY_STATE_BYTES,
|
|
68
|
+
RELAY_WRITES_PER_SECOND,
|
|
69
|
+
RELAY_WRITE_CAPS,
|
|
60
70
|
RPC_TIMEOUT_MS,
|
|
61
71
|
RoomCore,
|
|
62
72
|
RoomFullError,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AttributeOptions, ProfileSnapshot } from '@irtio/protocol';
|
|
2
2
|
import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
|
|
3
3
|
import { RoomDefinition, RoomMode, Room, RewindView, Ctx, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, Rapier2dModule, Rapier2dWorld, Rapier2dRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
|
|
4
|
-
import {
|
|
4
|
+
import { n as RoomHost, p as RoomStats, L as LogLevel, i as RoomCoreApi, j as RoomCoreOptions, l as RoomEventKind, s as TimelineRecorderOptions, T as TimelineDump, o as RoomInspection, J as JoinOptions, e as JoinResult, d as HostCallResult } from './contract-Hk4SkDtW.js';
|
|
5
5
|
|
|
6
6
|
declare class Mulberry32 {
|
|
7
7
|
/** Current internal state (u32). Survives hibernation. */
|
|
@@ -153,6 +153,8 @@ interface RoomInternals {
|
|
|
153
153
|
readonly physics: PhysicsApi | undefined;
|
|
154
154
|
tick: number;
|
|
155
155
|
stopped: boolean;
|
|
156
|
+
/** D77: the wall clock hold deadlines are measured against. See `RoomCoreOptions.wallNow`. */
|
|
157
|
+
wallNow(): number;
|
|
156
158
|
/** Runs a room handler; a throw is logged and counted, never rethrown. */
|
|
157
159
|
guard<T>(name: string, fn: () => T): T | undefined;
|
|
158
160
|
/** D41: record this tick on the authoritative timeline. A no-op while the recorder is unarmed. */
|
|
@@ -696,6 +698,8 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
696
698
|
readonly stats: RoomStats;
|
|
697
699
|
tick: number;
|
|
698
700
|
stopped: boolean;
|
|
701
|
+
/** D77: the wall clock hold deadlines are measured against. See `RoomCoreOptions.wallNow`. */
|
|
702
|
+
readonly wallNow: () => number;
|
|
699
703
|
/**
|
|
700
704
|
* Called for every handler throw `tryRun` swallows, before it is logged. A test harness sets
|
|
701
705
|
* this so a room that breaks fails the test that broke it (bug 2): the runtime's job is to
|
|
@@ -873,6 +877,10 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
873
877
|
* `receive()` accepted, plus the join snapshots this room handed the host to wrap in a
|
|
874
878
|
* `WELCOME` (the host builds that frame, so the room never sees it — see the docs page).
|
|
875
879
|
*/
|
|
880
|
+
/** D77: the encoded state size, when a host set the state cap. See `caps.ts`. */
|
|
881
|
+
stateBytes(): number | undefined;
|
|
882
|
+
/** D77: how many records are held for players who have left. */
|
|
883
|
+
holdCount(): number;
|
|
876
884
|
profile(): ProfileSnapshot | undefined;
|
|
877
885
|
private badFrame;
|
|
878
886
|
receive(clientId: string, frame: Uint8Array): void;
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { R as RoomCore } from '../room-
|
|
2
|
-
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-
|
|
1
|
+
import { R as RoomCore } from '../room-0FZBGyke.js';
|
|
2
|
+
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-0FZBGyke.js';
|
|
3
3
|
import { ErrorCodeName, FrameType } from '@irtio/protocol';
|
|
4
4
|
import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
5
|
-
import {
|
|
5
|
+
import { n as RoomHost, L as LogLevel, i as RoomCoreApi, c as HostCall, d as HostCallResult, W as WriteCaps, p as RoomStats } from '../contract-Hk4SkDtW.js';
|
|
6
6
|
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -138,6 +138,9 @@ declare class HarnessHost implements RoomHost {
|
|
|
138
138
|
crashed(reason: string): void;
|
|
139
139
|
hostCall(reqId: number, call: HostCall): void;
|
|
140
140
|
private runHostCall;
|
|
141
|
+
/** D81: every clip the room wrote, in order: `clipId` -> the blob it was handed. */
|
|
142
|
+
readonly clips: Map<string, Uint8Array<ArrayBufferLike>>;
|
|
143
|
+
private nextClipId;
|
|
141
144
|
/**
|
|
142
145
|
* D44: the in-process harness has no supervisor, no sockets and therefore no loopback session
|
|
143
146
|
* to open. Recording the request rather than faking a session is the honest option: a test that
|
|
@@ -267,6 +270,16 @@ interface FakeClient<S extends AnySchema = AnySchema> {
|
|
|
267
270
|
readonly joinTick: number;
|
|
268
271
|
/** Sends a `WRITE` update for `entity[id]` masking exactly the patch's leaves. */
|
|
269
272
|
write(entity: string, id: string, patch: AnyRecord): void;
|
|
273
|
+
/** D77: sends a `WRITE` `add` — what a client of a `clientCreate` collection sends. */
|
|
274
|
+
add(entity: string, id: string, value: AnyRecord): void;
|
|
275
|
+
/** D77: sends a `WRITE` `remove`. */
|
|
276
|
+
remove(entity: string, id: string): void;
|
|
277
|
+
/** Non-fatal `ERROR` frames this client was sent, in order. The caps are read from here. */
|
|
278
|
+
readonly errors: readonly {
|
|
279
|
+
code: number;
|
|
280
|
+
message: string;
|
|
281
|
+
fatal: boolean;
|
|
282
|
+
}[];
|
|
270
283
|
/** Sends an arbitrary framed payload (protocol-violation tests). */
|
|
271
284
|
writeRaw(frame: Uint8Array): void;
|
|
272
285
|
/** Client → server RPC. Settles when the `REPLY` is delivered — `h.tick()` first, then `await`. */
|
|
@@ -295,41 +308,6 @@ interface FakeClient<S extends AnySchema = AnySchema> {
|
|
|
295
308
|
entity(name: string): EntityCollection;
|
|
296
309
|
}
|
|
297
310
|
|
|
298
|
-
/**
|
|
299
|
-
* `createRoomHarness` — the in-process room harness. It runs a real `RoomCore`
|
|
300
|
-
* against a fake clock and fake clients, in one synchronous, deterministic process. No sockets,
|
|
301
|
-
* no worker, no Node built-ins: it runs anywhere Vitest does.
|
|
302
|
-
*
|
|
303
|
-
* ## Timing model
|
|
304
|
-
*
|
|
305
|
-
* The only clock is `FakeClock`. `h.tick(n)` advances exactly `n` tick intervals in tick mode
|
|
306
|
-
* (running `n` ticks and delivering every frame they produce) and runs due timers without moving
|
|
307
|
-
* the clock in event mode. `h.run(ms)` advances the clock by `ms`, firing ticks and timers in
|
|
308
|
-
* order. Both return only after everything due has fired and every frame has been delivered.
|
|
309
|
-
*
|
|
310
|
-
* ## Frame delivery
|
|
311
|
-
*
|
|
312
|
-
* Frames the room sends are delivered to the `FakeClient` immediately (decoded, applied to its
|
|
313
|
-
* view, traced). Frames a client sends are queued and handed to `RoomCore.receive` at the next
|
|
314
|
-
* safe point — never re-entrantly inside the core — which is before `tick()`/`run()`/`join()`
|
|
315
|
-
* returns, or immediately if nothing is currently executing.
|
|
316
|
-
*
|
|
317
|
-
* ## Promises
|
|
318
|
-
*
|
|
319
|
-
* `client.call()` settles when the `REPLY` is delivered, which happens inside a later
|
|
320
|
-
* `h.tick()` / `h.run()`. Tests therefore read:
|
|
321
|
-
*
|
|
322
|
-
* ```ts
|
|
323
|
-
* const p = a.call('start');
|
|
324
|
-
* h.tick();
|
|
325
|
-
* await expect(p).resolves.toEqual(...);
|
|
326
|
-
* ```
|
|
327
|
-
*
|
|
328
|
-
* An **async** `onCall` implementation cannot reply synchronously: its `REPLY` is sent when the
|
|
329
|
-
* promise settles, i.e. in a microtask. Tests must `await` (anything — `await null` is enough)
|
|
330
|
-
* and then `h.tick()` so the room sees the reply.
|
|
331
|
-
*/
|
|
332
|
-
|
|
333
311
|
interface HarnessOptions {
|
|
334
312
|
/** `room.random()` seed; defaults to a hash of `roomId`, exactly as `RoomCore` does. */
|
|
335
313
|
readonly seed?: number;
|
|
@@ -339,12 +317,22 @@ interface HarnessOptions {
|
|
|
339
317
|
readonly roomId?: string;
|
|
340
318
|
/** Bytes from a previous `serialize()`: the room wakes instead of being created. */
|
|
341
319
|
readonly restoreFrom?: Uint8Array;
|
|
320
|
+
/** D77 / O28: the write-core caps this room runs under. Default: the record cap only. */
|
|
321
|
+
readonly caps?: WriteCaps;
|
|
322
|
+
/** D77: the wall clock hold deadlines are measured against. Default `Date.now`. */
|
|
323
|
+
readonly wallNow?: () => number;
|
|
342
324
|
}
|
|
343
325
|
interface JoinSpec {
|
|
344
326
|
readonly role?: string;
|
|
345
327
|
readonly name?: string;
|
|
346
328
|
/** Default: `c1`, `c2`, … in join order. */
|
|
347
329
|
readonly id?: string;
|
|
330
|
+
/**
|
|
331
|
+
* D77: the identity a hold binds to. Defaults to the client id, exactly as a real join does
|
|
332
|
+
* when the client presented no assertion — which is what makes the "another tab reclaims" and
|
|
333
|
+
* "a new browser does not" pair testable without a socket.
|
|
334
|
+
*/
|
|
335
|
+
readonly playerId?: string;
|
|
348
336
|
}
|
|
349
337
|
interface UntilOptions {
|
|
350
338
|
/** Default 1000. */
|
package/dist/test/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
initPhysics,
|
|
6
6
|
initRapier2d,
|
|
7
7
|
visibleNames
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-GYPUNTQN.js";
|
|
9
9
|
|
|
10
10
|
// src/test/clock.ts
|
|
11
11
|
var FakeClock = class {
|
|
@@ -203,8 +203,19 @@ var HarnessHost = class {
|
|
|
203
203
|
if (existing === void 0 || call.score > existing) this.scores.set(key, call.score);
|
|
204
204
|
return { ok: true };
|
|
205
205
|
}
|
|
206
|
+
// ---- M6 lane N: replays ----
|
|
207
|
+
case "clip": {
|
|
208
|
+
const clipId = `clip${String(this.nextClipId++).padStart(4, "0")}`;
|
|
209
|
+
this.clips.set(clipId, call.bytes);
|
|
210
|
+
return { ok: true, value: clipId };
|
|
211
|
+
}
|
|
206
212
|
}
|
|
207
213
|
}
|
|
214
|
+
// ---- M6 lane N: replays ----
|
|
215
|
+
/** D81: every clip the room wrote, in order: `clipId` -> the blob it was handed. */
|
|
216
|
+
clips = /* @__PURE__ */ new Map();
|
|
217
|
+
nextClipId = 1;
|
|
218
|
+
// ---- end M6 lane N ----
|
|
208
219
|
// -------------------------------------------------------------------------
|
|
209
220
|
// Week 12: durable alarms (D26)
|
|
210
221
|
// -------------------------------------------------------------------------
|
|
@@ -293,6 +304,7 @@ import {
|
|
|
293
304
|
import {
|
|
294
305
|
FrameType,
|
|
295
306
|
decodeCall,
|
|
307
|
+
decodeErrorPayload,
|
|
296
308
|
decodeMsg,
|
|
297
309
|
decodeReply,
|
|
298
310
|
encodeCall,
|
|
@@ -311,7 +323,9 @@ import {
|
|
|
311
323
|
decodeSnapshot,
|
|
312
324
|
encodeDelta,
|
|
313
325
|
encodeFields,
|
|
314
|
-
|
|
326
|
+
markAdd,
|
|
327
|
+
markField,
|
|
328
|
+
markRemove
|
|
315
329
|
} from "@irtio/schema";
|
|
316
330
|
function structFields(t) {
|
|
317
331
|
return Object.entries(t.fields).map(([name, type]) => ({ name, type }));
|
|
@@ -427,6 +441,34 @@ var FakeClientImpl = class {
|
|
|
427
441
|
writeRaw(frame) {
|
|
428
442
|
this.bridge.toCore(this.id, frame);
|
|
429
443
|
}
|
|
444
|
+
// ---- M6 lane K: stateful relay ----
|
|
445
|
+
errors = [];
|
|
446
|
+
add(entity, id, value) {
|
|
447
|
+
const scratch = createState(this.bridge.ext);
|
|
448
|
+
entityOf(scratch, entity).add(id, value, { owner: this.id });
|
|
449
|
+
const dirty = createDirtySet();
|
|
450
|
+
markAdd(dirty, entity, id);
|
|
451
|
+
this.bridge.toCore(
|
|
452
|
+
this.id,
|
|
453
|
+
encodeFrame(
|
|
454
|
+
FrameType.WRITE,
|
|
455
|
+
encodeDelta(this.bridge.ext, scratch, dirty, { tick: this.bridge.tick })
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
remove(entity, id) {
|
|
460
|
+
const scratch = createState(this.bridge.ext);
|
|
461
|
+
const dirty = createDirtySet();
|
|
462
|
+
markRemove(dirty, entity, id);
|
|
463
|
+
this.bridge.toCore(
|
|
464
|
+
this.id,
|
|
465
|
+
encodeFrame(
|
|
466
|
+
FrameType.WRITE,
|
|
467
|
+
encodeDelta(this.bridge.ext, scratch, dirty, { tick: this.bridge.tick })
|
|
468
|
+
)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
// ---- end M6 lane K ----
|
|
430
472
|
call(name, params = {}) {
|
|
431
473
|
const desc = this.descOf(name);
|
|
432
474
|
const reqId = this.nextReqId++;
|
|
@@ -505,6 +547,13 @@ var FakeClientImpl = class {
|
|
|
505
547
|
case FrameType.MSG:
|
|
506
548
|
this.handleMsg(payload);
|
|
507
549
|
return;
|
|
550
|
+
// ---- M6 lane K: stateful relay ----
|
|
551
|
+
case FrameType.ERROR: {
|
|
552
|
+
const e = decodeErrorPayload(payload);
|
|
553
|
+
this.errors.push({ code: e.code, message: e.message, fatal: e.fatal });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// ---- end M6 lane K ----
|
|
508
557
|
default:
|
|
509
558
|
return;
|
|
510
559
|
}
|
|
@@ -679,7 +728,11 @@ var Harness = class {
|
|
|
679
728
|
roomId: options.roomId ?? "test-room",
|
|
680
729
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
681
730
|
...options.publicUrl !== void 0 ? { publicUrl: options.publicUrl } : {},
|
|
682
|
-
...options.restoreFrom !== void 0 ? { restoreFrom: options.restoreFrom } : {}
|
|
731
|
+
...options.restoreFrom !== void 0 ? { restoreFrom: options.restoreFrom } : {},
|
|
732
|
+
// ---- M6 lane K: stateful relay ----
|
|
733
|
+
...options.caps !== void 0 ? { caps: options.caps } : {},
|
|
734
|
+
...options.wallNow !== void 0 ? { wallNow: options.wallNow } : {}
|
|
735
|
+
// ---- end M6 lane K ----
|
|
683
736
|
});
|
|
684
737
|
this.host.core = this.coreRef;
|
|
685
738
|
this.host.serializeForSave = () => this.coreRef.snapshot();
|
|
@@ -881,7 +934,10 @@ var Harness = class {
|
|
|
881
934
|
const role = spec.role ?? roles[0] ?? "";
|
|
882
935
|
const options = {
|
|
883
936
|
role,
|
|
884
|
-
...spec.name !== void 0 ? { name: spec.name } : {}
|
|
937
|
+
...spec.name !== void 0 ? { name: spec.name } : {},
|
|
938
|
+
// ---- M6 lane K: stateful relay ----
|
|
939
|
+
...spec.playerId !== void 0 ? { playerId: spec.playerId } : {}
|
|
940
|
+
// ---- end M6 lane K ----
|
|
885
941
|
};
|
|
886
942
|
const client = new FakeClientImpl(this.bridge, id, options);
|
|
887
943
|
this.byId.set(id, client);
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LogLevel, T as TimelineDump,
|
|
1
|
+
import { L as LogLevel, T as TimelineDump, p as RoomStats, c as HostCall, d as HostCallResult, i as RoomCoreApi, n as RoomHost } from '../contract-Hk4SkDtW.js';
|
|
2
2
|
import { ErrorCodeName, ProfileSnapshot } from '@irtio/protocol';
|
|
3
3
|
import { NpcConfig, LeaveReason } from '@irtio/server';
|
|
4
4
|
import '@irtio/schema';
|
package/dist/worker/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-ACC3SDIO.js";
|
|
4
4
|
import {
|
|
5
5
|
RoomCore,
|
|
6
6
|
RoomFullError,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
initRapier2d,
|
|
10
10
|
onFirstRapierStep,
|
|
11
11
|
rapierHasStepped
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-GYPUNTQN.js";
|
|
13
13
|
|
|
14
14
|
// src/worker/index.ts
|
|
15
15
|
import { getHeapStatistics } from "v8";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"@dimforge/rapier2d-compat": "0.20.0",
|
|
32
32
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
33
33
|
"matter-js": "0.20.0",
|
|
34
|
-
"@irtio/protocol": "0.
|
|
35
|
-
"@irtio/schema": "0.
|
|
36
|
-
"@irtio/server": "0.
|
|
34
|
+
"@irtio/protocol": "0.11.0",
|
|
35
|
+
"@irtio/schema": "0.11.0",
|
|
36
|
+
"@irtio/server": "0.11.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/matter-js": "0.20.2"
|