@irtio/runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/chunk-3DQHT4CM.js +129 -0
- package/dist/chunk-X5S364FY.js +1573 -0
- package/dist/contract-BhD88PGb.d.ts +120 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +48 -0
- package/dist/room-CBsSCueH.d.ts +246 -0
- package/dist/test/index.d.ts +283 -0
- package/dist/test/index.js +774 -0
- package/dist/worker/index.d.ts +173 -0
- package/dist/worker/index.js +280 -0
- package/package.json +39 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { ErrorCodeName, FrameType } from '@irtio/protocol';
|
|
2
|
+
import { f as RoomHost, L as LogLevel, h as RoomStats } from '../contract-BhD88PGb.js';
|
|
3
|
+
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
4
|
+
import { LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
5
|
+
import { R as RoomCore } from '../room-CBsSCueH.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The harness clock: a deterministic fake `now()` plus an ordered timer queue. `RoomCore` only
|
|
9
|
+
* ever asks the host for `now()` / `setTimeout` / `clearTimeout`, so this is the whole notion of
|
|
10
|
+
* time in a harness room. Nothing here touches real timers, `Date`, or Node built-ins.
|
|
11
|
+
*/
|
|
12
|
+
declare class FakeClock {
|
|
13
|
+
/** Current fake time in ms. Starts at 0. */
|
|
14
|
+
time: number;
|
|
15
|
+
private readonly timers;
|
|
16
|
+
private nextId;
|
|
17
|
+
now(): number;
|
|
18
|
+
setTimeout(fn: () => void, ms: number): number;
|
|
19
|
+
clearTimeout(handle: unknown): void;
|
|
20
|
+
/** Number of timers still armed (a room that is ticking always has at least one). */
|
|
21
|
+
get pending(): number;
|
|
22
|
+
/**
|
|
23
|
+
* Advances to `time + ms`, firing every timer that comes due in order (earliest `at` first,
|
|
24
|
+
* ties broken by creation order). A timer that arms another timer inside the window fires too.
|
|
25
|
+
*/
|
|
26
|
+
advance(ms: number): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `HarnessHost` — the `RoomHost` the harness gives `RoomCore`. Everything the host is told is
|
|
31
|
+
* recorded so a test can assert on it: logs, kicks, `close()`, `sleep()`, `crashed()`, and how
|
|
32
|
+
* many frames/bytes went out (per client and in total).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
interface KickRecord {
|
|
36
|
+
readonly clientId: string;
|
|
37
|
+
readonly code: ErrorCodeName;
|
|
38
|
+
readonly reason: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
interface LogRecord {
|
|
41
|
+
readonly level: LogLevel;
|
|
42
|
+
readonly args: unknown[];
|
|
43
|
+
}
|
|
44
|
+
interface SendCounts {
|
|
45
|
+
frames: number;
|
|
46
|
+
bytes: number;
|
|
47
|
+
}
|
|
48
|
+
declare class HarnessHost implements RoomHost {
|
|
49
|
+
private readonly clock;
|
|
50
|
+
readonly logs: LogRecord[];
|
|
51
|
+
readonly kicks: KickRecord[];
|
|
52
|
+
/** `room.close()` reasons, in order. */
|
|
53
|
+
readonly closes: (string | undefined)[];
|
|
54
|
+
/** `host.crashed()` reasons, in order. */
|
|
55
|
+
readonly crashes: string[];
|
|
56
|
+
/** How many times the runtime asked to hibernate. */
|
|
57
|
+
sleeps: number;
|
|
58
|
+
/** Total frames handed to the host. */
|
|
59
|
+
framesSent: number;
|
|
60
|
+
bytesSent: number;
|
|
61
|
+
readonly sentByClient: Map<string, SendCounts>;
|
|
62
|
+
/** Set by the harness: where an outbound frame goes. */
|
|
63
|
+
onSend: (clientId: string, frame: Uint8Array) => void;
|
|
64
|
+
constructor(clock: FakeClock);
|
|
65
|
+
/** `true` once the runtime has asked to hibernate at least once. */
|
|
66
|
+
get slept(): boolean;
|
|
67
|
+
/** `true` once `room.close()` has been called. */
|
|
68
|
+
get closed(): boolean;
|
|
69
|
+
now(): number;
|
|
70
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
71
|
+
clearTimeout(handle: unknown): void;
|
|
72
|
+
send(clientId: string, frame: Uint8Array): void;
|
|
73
|
+
kick(clientId: string, code: ErrorCodeName, reason?: string): void;
|
|
74
|
+
close(reason?: string): void;
|
|
75
|
+
sleep(): void;
|
|
76
|
+
log(level: LogLevel, args: unknown[]): void;
|
|
77
|
+
crashed(reason: string): void;
|
|
78
|
+
/** Logged messages of one level, flattened to strings (handy in assertions). */
|
|
79
|
+
logsOf(level: LogLevel): string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Shared harness value types: the frame trace and the visibility-leak report. */
|
|
83
|
+
|
|
84
|
+
type FrameTypeName = keyof typeof FrameType;
|
|
85
|
+
declare function frameTypeName(type: number): FrameTypeName;
|
|
86
|
+
/** One frame crossing the harness seam. `in` = client → room, `out` = room → client. */
|
|
87
|
+
interface TraceEntry {
|
|
88
|
+
readonly dir: 'in' | 'out';
|
|
89
|
+
readonly clientId: string;
|
|
90
|
+
readonly frame: FrameTypeName;
|
|
91
|
+
/** The room's tick counter when the frame crossed. */
|
|
92
|
+
readonly tick: number;
|
|
93
|
+
readonly bytes: number;
|
|
94
|
+
/** Fake-clock ms when the frame crossed. */
|
|
95
|
+
readonly at: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A collection a client can see that its role must not.
|
|
99
|
+
*
|
|
100
|
+
* - `kind: 'view'` — the client's decoded state holds records of an invisible collection.
|
|
101
|
+
* - `kind: 'frame'` — a `DELTA`/`CORRECT` the client received named an invisible collection
|
|
102
|
+
* (caught while the frame was applied, even if the ops later cancelled out).
|
|
103
|
+
*/
|
|
104
|
+
interface VisibilityLeak {
|
|
105
|
+
readonly clientId: string;
|
|
106
|
+
readonly role: string;
|
|
107
|
+
readonly collection: string;
|
|
108
|
+
readonly kind: 'view' | 'frame';
|
|
109
|
+
/** Ids involved (`['']` for a singleton). */
|
|
110
|
+
readonly ids: readonly string[];
|
|
111
|
+
readonly tick: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The harness's fake client: what a real `@irtio/client` will do, with none of its semantics.
|
|
116
|
+
*
|
|
117
|
+
* It keeps a `PlainState` **view** of the extended schema — rebuilt from the join snapshot and
|
|
118
|
+
* advanced by every `DELTA` (and `CORRECT`, which carries the server's current values, so
|
|
119
|
+
* applying it is idempotent with the delta that follows it). It never predicts: a `write()` is
|
|
120
|
+
* only a frame, so a rejected write leaves the view exactly where the server left it.
|
|
121
|
+
*
|
|
122
|
+
* Everything the client sends goes through the harness's pump, so frames are delivered to the
|
|
123
|
+
* room at a safe point (never re-entrantly inside `RoomCore`).
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
type AnyRecord = Record<string, unknown>;
|
|
127
|
+
interface ReceivedCall {
|
|
128
|
+
readonly name: string;
|
|
129
|
+
readonly params: AnyRecord;
|
|
130
|
+
}
|
|
131
|
+
interface ReceivedMessage {
|
|
132
|
+
/** `'server'` for `room.send`, otherwise the relaying client's id. */
|
|
133
|
+
readonly from: 'server' | string;
|
|
134
|
+
readonly bytes: Uint8Array;
|
|
135
|
+
}
|
|
136
|
+
interface ReceivedCorrection {
|
|
137
|
+
readonly collection: string;
|
|
138
|
+
readonly id: string;
|
|
139
|
+
/** Only the fields the `CORRECT` carried. */
|
|
140
|
+
readonly patch: AnyRecord;
|
|
141
|
+
readonly tick: number;
|
|
142
|
+
}
|
|
143
|
+
type CallImpl = (params: AnyRecord) => unknown;
|
|
144
|
+
interface FakeClient<S extends AnySchema = AnySchema> {
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly role: string;
|
|
147
|
+
readonly name: string;
|
|
148
|
+
readonly connected: boolean;
|
|
149
|
+
/** Decoded state of the extended schema, as this client sees it. */
|
|
150
|
+
readonly view: PlainState;
|
|
151
|
+
/** The room tick this client (last) joined at. */
|
|
152
|
+
readonly joinTick: number;
|
|
153
|
+
/** Sends a `WRITE` update for `entity[id]` masking exactly the patch's leaves. */
|
|
154
|
+
write(entity: string, id: string, patch: AnyRecord): void;
|
|
155
|
+
/** Sends an arbitrary framed payload (protocol-violation tests). */
|
|
156
|
+
writeRaw(frame: Uint8Array): void;
|
|
157
|
+
/** Client → server RPC. Settles when the `REPLY` is delivered — `h.tick()` first, then `await`. */
|
|
158
|
+
call(name: string, params?: AnyRecord): Promise<unknown>;
|
|
159
|
+
/** The built-in `requestOwnership` RPC. */
|
|
160
|
+
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
161
|
+
/** Out-of-band `MSG`. */
|
|
162
|
+
send(target: 'all' | string | {
|
|
163
|
+
role: string;
|
|
164
|
+
}, bytes: Uint8Array): void;
|
|
165
|
+
/** Implements a client-direction RPC. Sync impls reply on delivery; async ones when they settle. */
|
|
166
|
+
onCall(name: string, impl: CallImpl): void;
|
|
167
|
+
readonly calls: readonly ReceivedCall[];
|
|
168
|
+
readonly messages: readonly ReceivedMessage[];
|
|
169
|
+
readonly corrections: readonly ReceivedCorrection[];
|
|
170
|
+
/** This client's slice of the harness trace. */
|
|
171
|
+
readonly frames: readonly TraceEntry[];
|
|
172
|
+
/** Opens the reconnect grace window: presence goes `connected: false`, frames stop. */
|
|
173
|
+
disconnect(): void;
|
|
174
|
+
/** Re-joins with the same id and role; the view is rebuilt from a fresh snapshot. */
|
|
175
|
+
reconnect(): void;
|
|
176
|
+
leave(reason?: LeaveReason): void;
|
|
177
|
+
/** A record from the view (`undefined` when absent). */
|
|
178
|
+
get(entity: string, id: string): AnyRecord | undefined;
|
|
179
|
+
/** A view collection, for direct inspection. */
|
|
180
|
+
entity(name: string): EntityCollection;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `createRoomHarness` — the in-process room harness. It runs a real `RoomCore`
|
|
185
|
+
* against a fake clock and fake clients, in one synchronous, deterministic process. No sockets,
|
|
186
|
+
* no worker, no Node built-ins: it runs anywhere Vitest does.
|
|
187
|
+
*
|
|
188
|
+
* ## Timing model
|
|
189
|
+
*
|
|
190
|
+
* The only clock is `FakeClock`. `h.tick(n)` advances exactly `n` tick intervals in tick mode
|
|
191
|
+
* (running `n` ticks and delivering every frame they produce) and runs due timers without moving
|
|
192
|
+
* the clock in event mode. `h.run(ms)` advances the clock by `ms`, firing ticks and timers in
|
|
193
|
+
* order. Both return only after everything due has fired and every frame has been delivered.
|
|
194
|
+
*
|
|
195
|
+
* ## Frame delivery
|
|
196
|
+
*
|
|
197
|
+
* Frames the room sends are delivered to the `FakeClient` immediately (decoded, applied to its
|
|
198
|
+
* view, traced). Frames a client sends are queued and handed to `RoomCore.receive` at the next
|
|
199
|
+
* safe point — never re-entrantly inside the core — which is before `tick()`/`run()`/`join()`
|
|
200
|
+
* returns, or immediately if nothing is currently executing.
|
|
201
|
+
*
|
|
202
|
+
* ## Promises
|
|
203
|
+
*
|
|
204
|
+
* `client.call()` settles when the `REPLY` is delivered, which happens inside a later
|
|
205
|
+
* `h.tick()` / `h.run()`. Tests therefore read:
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* const p = a.call('start');
|
|
209
|
+
* h.tick();
|
|
210
|
+
* await expect(p).resolves.toEqual(...);
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* An **async** `onCall` implementation cannot reply synchronously: its `REPLY` is sent when the
|
|
214
|
+
* promise settles, i.e. in a microtask. Tests must `await` (anything — `await null` is enough)
|
|
215
|
+
* and then `h.tick()` so the room sees the reply.
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
interface HarnessOptions {
|
|
219
|
+
/** `room.random()` seed; default 1 (same as `RoomCore`). */
|
|
220
|
+
readonly seed?: number;
|
|
221
|
+
/** Base URL for `room.link`. */
|
|
222
|
+
readonly publicUrl?: string;
|
|
223
|
+
/** Default `'test-room'`. */
|
|
224
|
+
readonly roomId?: string;
|
|
225
|
+
/** Bytes from a previous `serialize()`: the room wakes instead of being created. */
|
|
226
|
+
readonly restoreFrom?: Uint8Array;
|
|
227
|
+
}
|
|
228
|
+
interface JoinSpec {
|
|
229
|
+
readonly role?: string;
|
|
230
|
+
readonly name?: string;
|
|
231
|
+
/** Default: `c1`, `c2`, … in join order. */
|
|
232
|
+
readonly id?: string;
|
|
233
|
+
}
|
|
234
|
+
interface UntilOptions {
|
|
235
|
+
/** Default 1000. */
|
|
236
|
+
readonly maxTicks?: number;
|
|
237
|
+
}
|
|
238
|
+
interface RoomHarness<S extends AnySchema = AnySchema> {
|
|
239
|
+
/** Escape hatch: the real `RoomCore`. */
|
|
240
|
+
readonly core: RoomCore<S>;
|
|
241
|
+
/** The `room` API handlers see. */
|
|
242
|
+
readonly room: Room<S>;
|
|
243
|
+
/** The tracked authority — tests may mutate it exactly like room code. */
|
|
244
|
+
readonly state: State<S>;
|
|
245
|
+
/** The authority's plain state (what the codec reads). */
|
|
246
|
+
readonly plain: PlainState;
|
|
247
|
+
readonly stats: RoomStats;
|
|
248
|
+
readonly now: number;
|
|
249
|
+
/**
|
|
250
|
+
* The room's tick counter. Named `tickCount` because `tick()` is the method that advances it —
|
|
251
|
+
* TypeScript cannot have both under one name. `h.core.tick` is the same number.
|
|
252
|
+
*/
|
|
253
|
+
readonly tickCount: number;
|
|
254
|
+
readonly mode: 'tick' | 'event';
|
|
255
|
+
readonly host: HarnessHost;
|
|
256
|
+
/** Every client that ever joined, in join order. Departed ones stay, with `connected: false`. */
|
|
257
|
+
readonly clients: readonly FakeClient<S>[];
|
|
258
|
+
/** Every frame that crossed the seam, in order. */
|
|
259
|
+
readonly trace: readonly TraceEntry[];
|
|
260
|
+
/** Tick mode: run exactly `n` ticks. Event mode: run timers already due (the clock stays put). */
|
|
261
|
+
tick(n?: number): void;
|
|
262
|
+
/** Advance the fake clock, firing ticks and timers in order. */
|
|
263
|
+
run(ms: number): void;
|
|
264
|
+
/** Tick until `pred()` holds; throws when it never does. */
|
|
265
|
+
until(pred: () => boolean, options?: UntilOptions): void;
|
|
266
|
+
join(spec?: JoinSpec): FakeClient<S>;
|
|
267
|
+
join(n: number, spec?: JoinSpec): FakeClient<S>[];
|
|
268
|
+
/** Collections a client can see that its role must not. `[]` when the room is clean. */
|
|
269
|
+
checkVisibility(): VisibilityLeak[];
|
|
270
|
+
/** Hibernation bytes (`RoomCore.serialize`). */
|
|
271
|
+
serialize(): Uint8Array;
|
|
272
|
+
/**
|
|
273
|
+
* Replaces the room with a fresh `RoomCore` restored from `bytes`, keeping the same host and
|
|
274
|
+
* clock. Presence is empty after a restore, so the harness's client list is cleared too: the
|
|
275
|
+
* `FakeClient` objects from before are stale and must not be used again — re-`join()` instead.
|
|
276
|
+
*/
|
|
277
|
+
restore(bytes: Uint8Array): void;
|
|
278
|
+
stop(): void;
|
|
279
|
+
}
|
|
280
|
+
/** Creates a room harness around a `RoomDefinition`. The room is already started on return. */
|
|
281
|
+
declare function createRoomHarness<S extends AnySchema>(definition: RoomDefinition<S>, options?: HarnessOptions): RoomHarness<S>;
|
|
282
|
+
|
|
283
|
+
export { type CallImpl, type FakeClient, FakeClock, type FrameTypeName, HarnessHost, type HarnessOptions, type JoinSpec, type KickRecord, type LogRecord, type ReceivedCall, type ReceivedCorrection, type ReceivedMessage, type RoomHarness, type SendCounts, type TraceEntry, type UntilOptions, type VisibilityLeak, createRoomHarness, frameTypeName };
|