@moku-labs/game 0.0.1 → 0.0.2

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.
@@ -0,0 +1,2896 @@
1
+ import { Log } from "@moku-labs/common/browser";
2
+ import { AnyPluginInstance, PluginCtx } from "@moku-labs/core";
3
+
4
+ //#region src/config.d.ts
5
+ /**
6
+ * Global configuration of a game.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * createApp({ config: { orientation: "portrait", referenceSide: 1080, referenceLong: 2100 } });
11
+ * ```
12
+ */
13
+ type Config$5 = {
14
+ /** Screen orientation the game is designed for. */orientation: "portrait" | "landscape"; /** Short side of the reference resolution in pixels. */
15
+ referenceSide: number;
16
+ /**
17
+ * The long side, in reference units, the layout needs inside the safe area. The viewport scale
18
+ * fits both sides, so a wide screen gives the layout more width instead of less height.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // A board column of 2084 u: on a 768x1024 tablet the scale is 1024 / 2100 = 0.488 and the
23
+ * // reference width grows to 1575 u, so the whole column fits and nothing shrinks alone.
24
+ * createApp({ config: { referenceLong: 2100 } });
25
+ * ```
26
+ */
27
+ referenceLong: number;
28
+ };
29
+ /**
30
+ * Global events. Empty: every event belongs to a plugin.
31
+ */
32
+ type Events$3 = Record<never, never>;
33
+ /**
34
+ * Public API type of a plugin instance, read from its phantom carrier.
35
+ * Mirrors the kernel's non-exported `ExtractPluginApi`.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * type TimeApi = ApiOf<typeof timePlugin>; // the `Api` type of src/plugins/time/types.ts
40
+ * ```
41
+ */
42
+ type ApiOf<Plugin> = Plugin extends {
43
+ readonly _phantom: {
44
+ readonly api: infer PluginApi;
45
+ };
46
+ } ? PluginApi : never;
47
+ /**
48
+ * Structural type of `ctx.require`, for domain factories that resolve their own dependencies.
49
+ * The bound repeats the kernel's plugin shape so the kernel's generic `require` is assignable to it.
50
+ */
51
+ type Require = <Plugin extends {
52
+ readonly name: string;
53
+ readonly spec: unknown;
54
+ readonly _phantom: {
55
+ readonly config: unknown;
56
+ readonly state: unknown;
57
+ readonly api: unknown;
58
+ readonly events: Record<string, unknown>;
59
+ };
60
+ }>(plugin: Plugin) => ApiOf<Plugin>;
61
+ declare namespace types_d_exports$4 {
62
+ export { Api$4 as Api, ClockCtx, ClockSource, Config$4 as Config, Elapsed, FakeClock, State$4 as State };
63
+ }
64
+ /**
65
+ * Source of time and timers. The system source is the default; a game passes its own when time
66
+ * must come from somewhere else, for example a server.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * // Server time: the device clock only measures the distance from the last sync. The plugin keeps
71
+ * // at most one timer alive, so the source remembers one.
72
+ * const serverClock = (serverNow: number): ClockSource => {
73
+ * const syncedAt = performance.now();
74
+ * let timer: ReturnType<typeof setTimeout> | undefined;
75
+ *
76
+ * return {
77
+ * now: () => serverNow + (performance.now() - syncedAt),
78
+ * setTimer: (callback, delayMs) => (timer = setTimeout(callback, delayMs)),
79
+ * clearTimer: () => clearTimeout(timer)
80
+ * };
81
+ * };
82
+ *
83
+ * createApp({ pluginConfigs: { clock: { source: serverClock(1_790_000_000_000) } } });
84
+ * ```
85
+ */
86
+ type ClockSource = {
87
+ /**
88
+ * Reads the current moment.
89
+ *
90
+ * @returns Epoch milliseconds. A fraction is cut by the plugin; going back is tolerated.
91
+ */
92
+ now(): number;
93
+ /**
94
+ * Schedules one callback. The plugin keeps at most one timer alive.
95
+ *
96
+ * @param callback - Function to run when the delay has passed.
97
+ * @param delayMs - Delay in milliseconds, never negative.
98
+ * @returns A handle of any shape; the plugin only hands it back to `clearTimer`.
99
+ */
100
+ setTimer(callback: () => void, delayMs: number): unknown;
101
+ /**
102
+ * Cancels a pending timer. A handle this source did not produce must be ignored.
103
+ *
104
+ * @param handle - Handle returned by `setTimer`.
105
+ */
106
+ clearTimer(handle: unknown): void;
107
+ };
108
+ /**
109
+ * Fake source for tests, from `@moku-labs/game/testing`. It needs no real timer.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * // An energy point refills after 60 s. The test does not wait: it moves the clock.
114
+ * const clock = fakeClock(1000);
115
+ * const app = createApp({ pluginConfigs: { clock: { source: clock } } });
116
+ *
117
+ * app.clock.scheduleAt(61_000);
118
+ * clock.advance(60_000); // the listeners of onElapsed get { now: 61000 }
119
+ * ```
120
+ */
121
+ type FakeClock = ClockSource & {
122
+ /**
123
+ * Moves the clock forward and fires every timer that falls due on the way, in due order.
124
+ *
125
+ * @param ms - Milliseconds to move forward. A negative amount moves nothing.
126
+ * @example
127
+ * ```ts
128
+ * clock.advance(5000); // now() is 5000 later, a timer due in 3000 has fired
129
+ * ```
130
+ */
131
+ advance(ms: number): void;
132
+ /**
133
+ * Jumps to a moment without firing any timer. Moving back is allowed: that is a player who
134
+ * sets the device clock by hand.
135
+ *
136
+ * @param moment - Moment in epoch milliseconds.
137
+ * @example
138
+ * ```ts
139
+ * clock.set(400); // the device clock went back; app.clock.now() does not decrease
140
+ * ```
141
+ */
142
+ set(moment: number): void;
143
+ };
144
+ /**
145
+ * Input delivered to `onElapsed` listeners when a due moment arrives or the game resumes.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const input: Elapsed = { now: 1_790_000_060_000 };
150
+ * ```
151
+ */
152
+ type Elapsed = {
153
+ now: number;
154
+ };
155
+ /**
156
+ * clock plugin config.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * createApp({ pluginConfigs: { clock: { source: fakeClock(1000) } } });
161
+ * ```
162
+ */
163
+ type Config$4 = {
164
+ /**
165
+ * Time source. `undefined` means the system source: `Date.now` and `setTimeout`.
166
+ */
167
+ source: ClockSource | undefined;
168
+ };
169
+ /**
170
+ * clock plugin state.
171
+ */
172
+ type State$4 = {
173
+ source: ClockSource;
174
+ last: number;
175
+ dueAt: number | undefined;
176
+ handle: unknown;
177
+ listeners: Array<(input: Elapsed) => void>;
178
+ };
179
+ /**
180
+ * clock plugin API, `app.clock`. Trusted time for rules that depend on real time: energy refill,
181
+ * generators, daily rewards. The clock holds ONE pending moment; the rules decide the next one.
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * // The whole cycle: ask the rules for the nearest moment, wait for it, apply, ask again.
186
+ * app.clock.onElapsed(({ now }) => {
187
+ * applyRefills(now);
188
+ * app.clock.scheduleAt(nextRefillAt(now)); // undefined when nothing is pending
189
+ * });
190
+ * app.clock.scheduleAt(nextRefillAt(app.clock.now()));
191
+ * ```
192
+ */
193
+ type Api$4 = {
194
+ /**
195
+ * Reads trusted time. It never decreases, whatever the device clock does, so a player who sets
196
+ * the clock back gains nothing.
197
+ *
198
+ * @returns The current moment in epoch milliseconds, an integer.
199
+ * @example
200
+ * ```ts
201
+ * // Stamp a generator when it is used, and compare later.
202
+ * player.generator.usedAt = app.clock.now(); // 1790000000000
203
+ * const ready = app.clock.now() - player.generator.usedAt >= 60_000;
204
+ * ```
205
+ */
206
+ now(): number;
207
+ /**
208
+ * Replaces the single pending due moment. A moment in the past is not delivered synchronously:
209
+ * it fires on the next macrotask of the source.
210
+ *
211
+ * @param moment - Moment in epoch milliseconds, or `undefined` to cancel.
212
+ * @example
213
+ * ```ts
214
+ * // A generator refills in 60 s. Only one moment is pending: the nearest one.
215
+ * app.clock.scheduleAt(app.clock.now() + 60_000);
216
+ * app.clock.scheduleAt(undefined); // nothing is due any more: cancel
217
+ * ```
218
+ */
219
+ scheduleAt(moment: number | undefined): void;
220
+ /**
221
+ * Registers a listener for the `elapsed` input: a due moment arrived, or `poke` was called.
222
+ * The clock does not reschedule by itself.
223
+ *
224
+ * @param listener - Called with `{ now }`, the trusted moment of the delivery.
225
+ * @returns The unsubscribe function.
226
+ * @example
227
+ * ```ts
228
+ * const off = app.clock.onElapsed(({ now }) => {
229
+ * applyRefills(now); // now: 1790000060000
230
+ * });
231
+ *
232
+ * off(); // the screen that showed the timer is closed
233
+ * ```
234
+ */
235
+ onElapsed(listener: (input: Elapsed) => void): () => void;
236
+ /**
237
+ * Delivers `elapsed` right now, without touching the pending due moment. The engine calls it
238
+ * when the game resumes; a game calls it after it changed something the rules depend on.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * // The game synced with its server and the time source jumped forward: let the rules catch up.
243
+ * // On resume from background the engine pokes by itself, a game never does that.
244
+ * app.clock.poke(); // onElapsed listeners get { now } at once; the pending moment stays
245
+ * ```
246
+ */
247
+ poke(): void;
248
+ /**
249
+ * Reads the pending due moment, for inspection and tests.
250
+ *
251
+ * @returns The pending moment, or `undefined` when nothing is scheduled.
252
+ * @example
253
+ * ```ts
254
+ * app.clock.scheduleAt(1500);
255
+ * app.clock.dueAt(); // 1500
256
+ * await app.stop();
257
+ * app.clock.dueAt(); // undefined
258
+ * ```
259
+ */
260
+ dueAt(): number | undefined;
261
+ };
262
+ /**
263
+ * Domain context of the clock plugin.
264
+ */
265
+ type ClockCtx = PluginCtx<Config$4, State$4> & {
266
+ readonly global: object;
267
+ };
268
+ declare namespace types_d_exports$3 {
269
+ export { Api$3 as Api, Config$3 as Config, Events$2 as Events, LifecycleCtx, PauseReason, State$3 as State };
270
+ }
271
+ /**
272
+ * Why the game is paused. The named reasons are the engine's own; any other string is allowed.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * const reason: PauseReason = "background";
277
+ * ```
278
+ */
279
+ type PauseReason = "background" | "devtools" | "system-dialog" | "device-lost" | (string & {});
280
+ /**
281
+ * lifecycle plugin events.
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * // The payload of the first `app.lifecycle.push("background")`.
286
+ * const payload: Events["lifecycle:changed"] = {
287
+ * reason: "background", action: "push",
288
+ * reasons: ["background"], paused: true, resumed: false
289
+ * };
290
+ * ```
291
+ */
292
+ type Events$2 = {
293
+ /**
294
+ * The pause stack changed. `reason` and `action` say what changed; `resumed` is true only on
295
+ * the change that emptied the stack.
296
+ */
297
+ "lifecycle:changed": {
298
+ reason: PauseReason;
299
+ action: "push" | "pop";
300
+ reasons: readonly PauseReason[];
301
+ paused: boolean;
302
+ resumed: boolean;
303
+ };
304
+ };
305
+ /**
306
+ * lifecycle plugin config: none. The plugin has no tunable behaviour.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const config: Config = {};
311
+ * ```
312
+ */
313
+ type Config$3 = Record<string, never>;
314
+ /**
315
+ * lifecycle plugin state: the pause reasons in insertion order, no duplicates.
316
+ */
317
+ type State$3 = {
318
+ reasons: PauseReason[];
319
+ };
320
+ /**
321
+ * lifecycle plugin API, `app.lifecycle`. The stack of reasons why the game is paused: the first
322
+ * reason pauses `time`, the last one to leave resumes it, and every real change of the stack
323
+ * emits `lifecycle:changed`.
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * // Two reasons hold the pause, so they never cancel each other.
328
+ * app.lifecycle.push("background"); // time pauses
329
+ * app.lifecycle.push("ad"); // still one pause
330
+ * app.lifecycle.pop("background"); // still paused: "ad" holds
331
+ * app.lifecycle.pop("ad"); // time resumes
332
+ * ```
333
+ */
334
+ type Api$3 = {
335
+ /**
336
+ * Pushes a reason why the game is paused. A reason already on the stack is ignored, so two
337
+ * plugins pausing for the same reason never pause twice.
338
+ *
339
+ * @param reason - Why the game is paused.
340
+ * @example
341
+ * ```ts
342
+ * // A rewarded ad covers the game: the world stands while it plays.
343
+ * app.lifecycle.push("ad"); // app.time.isPaused() is true, "lifecycle:changed" is emitted
344
+ * app.lifecycle.push("ad"); // already on the stack: no second pause, no event
345
+ * ```
346
+ */
347
+ push(reason: PauseReason): void;
348
+ /**
349
+ * Pops a reason. The game runs again only when the last reason leaves. A reason that is not
350
+ * on the stack is ignored.
351
+ *
352
+ * @param reason - The reason that no longer holds.
353
+ * @example
354
+ * ```ts
355
+ * // The tab is visible again. The game runs, unless another reason still holds.
356
+ * document.addEventListener("visibilitychange", () => {
357
+ * if (!document.hidden) app.lifecycle.pop("background");
358
+ * });
359
+ * ```
360
+ */
361
+ pop(reason: PauseReason): void;
362
+ /**
363
+ * Reads the stack as a frozen copy, in insertion order, so a caller cannot write into it.
364
+ *
365
+ * @returns The pause reasons currently held.
366
+ * @example
367
+ * ```ts
368
+ * // A debug overlay shows why the game stands still.
369
+ * app.lifecycle.push("background");
370
+ * app.lifecycle.push("ad");
371
+ * app.lifecycle.reasons(); // ["background", "ad"]
372
+ * ```
373
+ */
374
+ reasons(): readonly PauseReason[];
375
+ /**
376
+ * Tells whether the game is paused: true while the stack is not empty.
377
+ *
378
+ * @returns True while at least one reason holds.
379
+ * @example
380
+ * ```ts
381
+ * // The idle hint must not start behind a system dialog.
382
+ * app.lifecycle.push("system-dialog");
383
+ * app.lifecycle.isPaused(); // true
384
+ * ```
385
+ */
386
+ isPaused(): boolean;
387
+ };
388
+ /**
389
+ * Domain context: the kernel context with `require`, used to reach `time`. `emit` is the kernel's:
390
+ * `index.ts` writes `events` with an annotated `register` (core spec `14` row 8), so the own event
391
+ * reaches a factory passed by direct reference.
392
+ */
393
+ type LifecycleCtx = PluginCtx<Config$3, State$3, Events$2> & {
394
+ readonly require: Require;
395
+ };
396
+ //#endregion
397
+ //#region src/plugins/model/rng/types.d.ts
398
+ /**
399
+ * @file model/rng — type definitions.
400
+ */
401
+ /**
402
+ * Persisted randomness: one uint32 per stream id.
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * // Seed 42, one draw from the stream "chest:42".
407
+ * const rng: RngState = { seed: 42, streams: { "chest:42": 22541618 } };
408
+ * ```
409
+ */
410
+ type RngState = {
411
+ seed: number;
412
+ streams: Record<string, number>;
413
+ };
414
+ /**
415
+ * One deterministic stream. Integers only, so every engine produces the same numbers.
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * // Inside a node the rng view comes with the context.
420
+ * export const roll = defineNode({
421
+ * outcomes: { done: type() },
422
+ * run: ({ player, rng, out }) => {
423
+ * player.coins += rng.stream("dice").range(1, 6); // 4: first draw of a save with seed 42
424
+ * return out.done();
425
+ * }
426
+ * });
427
+ * ```
428
+ */
429
+ type RngStream = {
430
+ /**
431
+ * Draws an integer in `[0, maxExclusive)`.
432
+ *
433
+ * @param maxExclusive - Upper bound, excluded.
434
+ * @returns The drawn integer.
435
+ * @throws {Error} When the bound is not a positive integer.
436
+ * @example
437
+ * ```ts
438
+ * // Which of the six board columns gets the new tile.
439
+ * const column = rng.stream("spawn").int(6); // 0 to 5
440
+ * ```
441
+ */
442
+ int(maxExclusive: number): number;
443
+ /**
444
+ * Draws an integer in `[min, maxInclusive]`.
445
+ *
446
+ * @param min - Lower bound, included.
447
+ * @param maxInclusive - Upper bound, included.
448
+ * @returns The drawn integer.
449
+ * @throws {Error} When the span is empty.
450
+ * @example
451
+ * ```ts
452
+ * // A die.
453
+ * player.lastRoll = rng.stream("dice").range(1, 6); // 4: first draw of a save with seed 42
454
+ * ```
455
+ */
456
+ range(min: number, maxInclusive: number): number;
457
+ /**
458
+ * Picks one element of an array.
459
+ *
460
+ * @param items - The array to pick from.
461
+ * @returns The picked element.
462
+ * @throws {Error} When the array is empty or has holes.
463
+ * @example
464
+ * ```ts
465
+ * // The reward of chest 42.
466
+ * rng.stream("chest:42").pick(["coin", "gem", "key"]); // "gem": first draw, seed 42
467
+ * ```
468
+ */
469
+ pick<Item>(items: readonly Item[]): Item;
470
+ /**
471
+ * Picks one entry of a table by its integer weight. Entries of weight zero never win.
472
+ *
473
+ * @param table - Entries with integer weights.
474
+ * @returns The picked entry.
475
+ * @throws {Error} When no entry has a positive weight.
476
+ * @example
477
+ * ```ts
478
+ * // A coin drops three times as often as a gem.
479
+ * const drop = rng.stream("drop").weighted([
480
+ * { id: "coin", weight: 3 },
481
+ * { id: "gem", weight: 1 }
482
+ * ]); // { id: "gem", weight: 1 }: first draw, seed 42
483
+ * ```
484
+ */
485
+ weighted<Entry extends {
486
+ weight: number;
487
+ }>(table: readonly Entry[]): Entry;
488
+ /**
489
+ * Draws a `numerator` in `denominator` chance.
490
+ *
491
+ * @param numerator - How many of the outcomes are a hit.
492
+ * @param denominator - How many outcomes there are.
493
+ * @returns True when the draw is a hit.
494
+ * @throws {Error} When the denominator is not a positive integer.
495
+ * @example
496
+ * ```ts
497
+ * // One merge in twenty gives a bonus tile.
498
+ * const bonus = rng.stream("bonus").chance(1, 20);
499
+ * ```
500
+ */
501
+ chance(numerator: number, denominator: number): boolean;
502
+ };
503
+ /**
504
+ * View over an rng branch, draft or frozen: `rng` of a node context. Every draw advances
505
+ * `streams[id]` of the branch, so draws inside a transaction commit with it and draws inside a
506
+ * discarded transaction are forgotten with it.
507
+ */
508
+ type RngView = {
509
+ /**
510
+ * Opens one stream. The first draw seeds it from the save's seed and the id, so opening a
511
+ * stream without drawing leaves the branch untouched.
512
+ *
513
+ * @param id - Stream id. One id per source, for example `"chest:42"`.
514
+ * @returns The stream of that id.
515
+ * @example
516
+ * ```ts
517
+ * // One id per source: a kill of the app before the rest node cannot re-roll chest 42.
518
+ * const chest = rng.stream("chest:42");
519
+ * chest.int(6); // 2: first draw of a save with seed 42
520
+ * chest.int(6); // 0
521
+ * ```
522
+ */
523
+ stream(id: string): RngStream;
524
+ };
525
+ /**
526
+ * rng module API, `app.model.rng`: inspection of stream state in the committed document. Draws
527
+ * never happen here; they happen on the `rng` view of a node context.
528
+ */
529
+ type RngApi = {
530
+ /**
531
+ * Reads the committed state of one stream, for bookmarks, tools and tests. It never draws.
532
+ *
533
+ * @param id - Stream id.
534
+ * @returns The uint32 state of the stream, or `undefined` when it was never drawn.
535
+ * @example
536
+ * ```ts
537
+ * // A test checks that one roll drew from the dice stream. The save has seed 42.
538
+ * app.model.rng.peek("dice"); // undefined: never drawn
539
+ * await game.walk([{ at: "home", intent: "roll" }]);
540
+ * app.model.rng.peek("dice"); // 1175946015
541
+ * ```
542
+ */
543
+ peek(id: string): number | undefined;
544
+ };
545
+ //#endregion
546
+ //#region src/plugins/model/store/types.d.ts
547
+ /**
548
+ * JSON patch produced by a commit.
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * const patch: Patch = { op: "replace", path: ["player", "coins"], value: 12 };
553
+ * ```
554
+ */
555
+ type Patch = {
556
+ op: "add" | "remove" | "replace";
557
+ path: (string | number)[];
558
+ value?: Json;
559
+ };
560
+ /**
561
+ * The persisted document.
562
+ *
563
+ * @example
564
+ * ```ts
565
+ * const doc: SaveDoc = { player: { coins: 0 }, rng: { seed: 42, streams: {} } };
566
+ * ```
567
+ */
568
+ type SaveDoc = {
569
+ player: Json;
570
+ rng: RngState;
571
+ };
572
+ /**
573
+ * The save seam implemented by the application layer. `state` is the whole SaveDoc.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * // A server save: the server holds the document and applies the patches it is sent.
578
+ * const serverSave = (url: string): PlayerStateProvider => {
579
+ * const queue: { patches: Patch[]; version: number }[] = [];
580
+ *
581
+ * const send = async (batch: typeof queue, txId?: string): Promise<void> => {
582
+ * if (batch.length === 0) return;
583
+ * await fetch(url, { method: "POST", body: JSON.stringify({ batch, txId }) });
584
+ * };
585
+ *
586
+ * return {
587
+ * load: async () => {
588
+ * const response = await fetch(url);
589
+ * return response.status === 404 ? null : await response.json(); // { state, version }
590
+ * },
591
+ * commit: (patches, version) => {
592
+ * queue.push({ patches, version }); // a rest node: accumulate, write at the next flush
593
+ * },
594
+ * commitDurable: async (patches, txId, version) => {
595
+ * queue.push({ patches, version });
596
+ * await send(queue.splice(0), txId); // a barrier node: resolve only when it is stored
597
+ * },
598
+ * flush: () => send(queue.splice(0))
599
+ * };
600
+ * };
601
+ *
602
+ * createApp({ pluginConfigs: { model: { playerProvider: serverSave("/api/save") } } });
603
+ * ```
604
+ */
605
+ type PlayerStateProvider = {
606
+ /**
607
+ * Reads the save. Called once, by `store.load()`.
608
+ *
609
+ * @returns The stored document and its schema version. `null` means a new player.
610
+ */
611
+ load(): Promise<{
612
+ state: Json;
613
+ version: number;
614
+ } | null>;
615
+ /**
616
+ * Called at rest nodes. The provider accumulates and debounces writes.
617
+ *
618
+ * @param patches - Doc patches since the last commit.
619
+ * @param version - Schema version this build writes.
620
+ */
621
+ commit(patches: Patch[], version: number): void;
622
+ /**
623
+ * Called after a barrier node. Resolves when the data is durable.
624
+ *
625
+ * @param patches - Doc patches since the last commit.
626
+ * @param txId - Id of the transaction that left the barrier node.
627
+ * @param version - Schema version this build writes.
628
+ * @returns Resolves when the data is durable.
629
+ */
630
+ commitDurable(patches: Patch[], txId: string, version: number): Promise<void>;
631
+ /**
632
+ * Writes what was accumulated. Called on a background pause and on stop.
633
+ *
634
+ * @returns Resolves when the provider has written.
635
+ */
636
+ flush(): Promise<void>;
637
+ };
638
+ /**
639
+ * One recorded call of the in-memory provider.
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * const call: ProviderCall = {
644
+ * method: "commit",
645
+ * patches: [{ op: "replace", path: ["player", "coins"], value: 4 }],
646
+ * version: 1
647
+ * };
648
+ * ```
649
+ */
650
+ type ProviderCall = {
651
+ method: "load";
652
+ } | {
653
+ method: "commit";
654
+ patches: Patch[];
655
+ version: number;
656
+ } | {
657
+ method: "commitDurable";
658
+ patches: Patch[];
659
+ txId: string;
660
+ version: number;
661
+ } | {
662
+ method: "flush";
663
+ };
664
+ /**
665
+ * One step of the save migration chain. `up` of `from: n` gets the whole save document of version
666
+ * `n` and returns the document of version `n + 1`.
667
+ *
668
+ * @example
669
+ * ```ts
670
+ * // Version 1 saved `gold`. Version 2 calls it `coins`.
671
+ * const renameGold: Migration = {
672
+ * from: 1,
673
+ * up: state => {
674
+ * const doc = state as { player: { gold: number }; rng: RngState };
675
+ *
676
+ * return { player: { coins: doc.player.gold }, rng: doc.rng };
677
+ * }
678
+ * };
679
+ *
680
+ * createApp({ pluginConfigs: { model: { schemaVersion: 2, migrations: [renameGold] } } });
681
+ * ```
682
+ */
683
+ type Migration = {
684
+ from: number;
685
+ up(state: Json): Json;
686
+ };
687
+ /**
688
+ * Frozen view of committed state.
689
+ *
690
+ * @example
691
+ * ```ts
692
+ * const snapshot: Snapshot = {
693
+ * player: { coins: 4 },
694
+ * session: { rolls: 1 },
695
+ * rng: { seed: 42, streams: { dice: 1175946015 } }
696
+ * };
697
+ * ```
698
+ */
699
+ type Snapshot = {
700
+ readonly player: Json;
701
+ readonly session: Json;
702
+ readonly rng: Readonly<RngState>;
703
+ };
704
+ /**
705
+ * Result of a commit: the patches split by tree, and the touched roots in the order `player`,
706
+ * `session`, `rng`.
707
+ *
708
+ * @example
709
+ * ```ts
710
+ * const result: CommitResult = {
711
+ * patches: { doc: [{ op: "replace", path: ["player", "coins"], value: 4 }], session: [] },
712
+ * roots: ["player"]
713
+ * };
714
+ * ```
715
+ */
716
+ type CommitResult = {
717
+ patches: {
718
+ doc: Patch[];
719
+ session: Patch[];
720
+ };
721
+ roots: Root[];
722
+ };
723
+ /**
724
+ * Open transaction handed to one node run.
725
+ *
726
+ * @example
727
+ * ```ts
728
+ * // A game never holds a transaction: the node context carries its drafts and its rng view.
729
+ * export const roll = defineNode({
730
+ * outcomes: { done: type() },
731
+ * run: ({ player, rng, out }) => {
732
+ * player.coins += rng.stream("dice").range(1, 6);
733
+ * return out.done();
734
+ * }
735
+ * });
736
+ * ```
737
+ */
738
+ type Transaction = {
739
+ /** Mutable draft of the player tree. */player: Json; /** Mutable draft of the session tree. */
740
+ session: Json; /** Rng view bound to the draft `doc.rng`. Draws advance the draft. */
741
+ rng: RngView;
742
+ /**
743
+ * Commits the drafts of this transaction on the edge: finishes the drafts, swaps the frozen
744
+ * trees, appends the doc patches to the pending list and emits `model:committed` with cause
745
+ * `"edge"`.
746
+ *
747
+ * @returns The patches split by tree and the touched roots.
748
+ * @throws {Error} When the transaction was already committed or discarded.
749
+ * @example
750
+ * ```ts
751
+ * // A runner plugin commits on the edge, after the node body returned its outcome.
752
+ * const { store } = ctx.require(modelPlugin);
753
+ * const transaction = store.begin();
754
+ * await runNodeBody(transaction); // the node wrote coins into transaction.player
755
+ * transaction.commit().roots; // ["player"]
756
+ * ```
757
+ */
758
+ commit(): CommitResult;
759
+ /**
760
+ * Drops the drafts of this transaction. Nothing changes and nothing is emitted.
761
+ *
762
+ * @throws {Error} When the transaction was already committed or discarded.
763
+ * @example
764
+ * ```ts
765
+ * // The node threw or was aborted: nothing of its drafts may reach the state.
766
+ * transaction.discard();
767
+ * store.snapshot().player; // as it was before begin()
768
+ * ```
769
+ */
770
+ discard(): void;
771
+ };
772
+ /**
773
+ * store module state.
774
+ */
775
+ type StoreState = {
776
+ /** Frozen save document. */doc: SaveDoc; /** Frozen session tree. */
777
+ session: Json; /** Frozen trees of the last rest point. */
778
+ restPoint: {
779
+ doc: SaveDoc;
780
+ session: Json;
781
+ } | undefined; /** Doc patches since the last provider commit. */
782
+ pending: Patch[];
783
+ transaction: Transaction | undefined;
784
+ loaded: boolean;
785
+ provider: PlayerStateProvider;
786
+ };
787
+ /**
788
+ * store module API, `app.model.store`. Logic changes the trees only inside a transaction, and only
789
+ * a rest point hands patches to the provider, so a kill between two rest nodes loses a whole
790
+ * transition and never half of one.
791
+ *
792
+ * @example
793
+ * ```ts
794
+ * // A game only reads the store. The flow runner loads, opens transactions and marks rest points.
795
+ * const { player, session } = app.model.store.snapshot();
796
+ * ```
797
+ */
798
+ type StoreApi = {
799
+ /**
800
+ * Loads the save. A new player gets the initial player and a seed; a stored save runs through
801
+ * the migration chain. Nothing is written until the document is complete, so a failure leaves
802
+ * the state exactly as it was and the app can show a clear screen. A document the provider has
803
+ * no base for, a new player or a migrated save, is handed over at once: a player who leaves on
804
+ * the first screen is a saved player. Marks the first rest point and emits `model:committed`
805
+ * with cause `"load"`.
806
+ *
807
+ * @returns Resolves when the document is in place.
808
+ * @throws {SaveUnreadableError} When the save cannot be read by this build.
809
+ * @throws {unknown} The error of a provider that fails to load or refuses the first commit.
810
+ * @example
811
+ * ```ts
812
+ * // The flow runner loads the save once, before the first node; a game calls app.flow.run().
813
+ * const { store } = ctx.require(modelPlugin);
814
+ * await store.load(); // model:committed fires with cause "load"
815
+ * store.snapshot().player; // { coins: 0 } for a new player, from config.initialPlayer
816
+ * ```
817
+ */
818
+ load(): Promise<void>;
819
+ /**
820
+ * Hands out the committed trees. Frozen: this is the only thing a view or a projection sees.
821
+ * Before `load()` it shows the initial player, never a half-read save. The same object comes
822
+ * back while nothing was committed, so a watcher compares identities instead of trees.
823
+ *
824
+ * @returns The frozen player, session and rng trees.
825
+ * @example
826
+ * ```ts
827
+ * // A test plays two rolls headless and reads what was committed.
828
+ * await game.walk([{ at: "home", intent: "roll" }, { at: "home", intent: "roll" }]);
829
+ * app.model.store.snapshot().session; // { rolls: 2 }
830
+ *
831
+ * // An editor panel re-reads the model only when a commit replaced the snapshot.
832
+ * app.model.store.snapshot() === app.model.store.snapshot(); // true until the next commit
833
+ * ```
834
+ */
835
+ snapshot(): Snapshot;
836
+ /**
837
+ * Opens the drafts of one node run. One transaction at a time: a second `begin` is a bug in the
838
+ * runner, not a state to recover from.
839
+ *
840
+ * @returns The open transaction.
841
+ * @throws {Error} When a transaction is already open.
842
+ * @example
843
+ * ```ts
844
+ * // The flow runner opens one transaction when it enters a node and keeps it until the edge.
845
+ * const transaction = ctx.require(modelPlugin).store.begin();
846
+ * transaction.player; // a draft: writes stay invisible to snapshot() until commit()
847
+ * ```
848
+ */
849
+ begin(): Transaction;
850
+ /**
851
+ * Marks a rest node: the provider receives everything since the last rest point, and only then
852
+ * does the rest point move. A throwing provider leaves both untouched, so the next attempt
853
+ * still holds the whole transition.
854
+ *
855
+ * @throws {unknown} The error of a failing provider.
856
+ * @example
857
+ * ```ts
858
+ * // The edge led into a node with `rest: true`: the provider gets the whole transition.
859
+ * transaction.commit();
860
+ * store.markRest(); // provider.commit() receives every patch since the last rest point
861
+ * ```
862
+ */
863
+ markRest(): void;
864
+ /**
865
+ * Marks a barrier node: rollback cannot cross it, so the rest point moves first. The pending
866
+ * patches are dropped only once the provider reports the data as durable.
867
+ *
868
+ * @param txId - Id of the transaction that left the barrier node.
869
+ * @returns Resolves when the data is durable.
870
+ * @throws {unknown} The error of a failing provider.
871
+ * @example
872
+ * ```ts
873
+ * // The edge left a node with `barrier: true`, for example a granted purchase: wait for the disk.
874
+ * // The id is path#journalIndex@now, built by the runner.
875
+ * await store.markBarrier("shop/grant#12@1790000000000");
876
+ * ```
877
+ */
878
+ markBarrier(txId: string): Promise<void>;
879
+ /**
880
+ * Returns to the last rest point. It discards an open transaction first. A pointer swap to
881
+ * frozen trees: no inverse patch is replayed. Everything in `pending` belongs to the failed
882
+ * transition, because a rest point empties it. Emits `model:committed` with cause `"rollback"`.
883
+ *
884
+ * @example
885
+ * ```ts
886
+ * // A node failed after it wrote into its drafts: back to the last rest point, then retry.
887
+ * store.rollback(); // model:committed fires with cause "rollback"
888
+ * ```
889
+ */
890
+ rollback(): void;
891
+ /**
892
+ * Replaces the trees: a bookmark, a repro, a dev restore after a reload. What is omitted stays
893
+ * as it is. The restored trees become the new rest point. The provider receives the whole
894
+ * document at the next rest point, never patches on a base it no longer has. Emits
895
+ * `model:committed` with cause `"restore"`.
896
+ *
897
+ * @param input - The trees to put in place.
898
+ * @param input.player - The player tree.
899
+ * @param input.session - The session tree. Omitted: the current one stays.
900
+ * @param input.rng - The rng branch. Omitted: the current one stays.
901
+ * @throws {Error} When a transaction is open.
902
+ * @example
903
+ * ```ts
904
+ * // The flow runner enters a bookmark: the trees first, then the rest point, then the node.
905
+ * // A game calls app.flow.restore(bookmark), which does all three.
906
+ * store.restore({ player: bookmark.player, session: bookmark.session, rng: bookmark.rng });
907
+ * store.markRest();
908
+ * ```
909
+ */
910
+ restore(input: {
911
+ player: Json;
912
+ session?: Json;
913
+ rng?: RngState;
914
+ }): void;
915
+ /**
916
+ * Asks the provider to write what it has accumulated. The pending patches stay: between two
917
+ * rest nodes they are an unfinished transition, and a kill must lose all of it, never half.
918
+ * The engine calls it on a background pause and on stop.
919
+ *
920
+ * @returns Resolves when the provider has written.
921
+ * @throws {unknown} The error of a failing provider.
922
+ * @example
923
+ * ```ts
924
+ * // The game leaves the page for a payment screen: write the save first.
925
+ * await app.model.store.flush();
926
+ * window.location.assign("/checkout");
927
+ * ```
928
+ */
929
+ flush(): Promise<void>;
930
+ };
931
+ /**
932
+ * Thrown by `load()`, and so by `flow.run()`, when the save cannot be read by this build. The
933
+ * save stays untouched, so the app can show a clear screen instead of overwriting it.
934
+ *
935
+ * @example
936
+ * ```ts
937
+ * // The player opens an old build over a newer save: show "update the game", keep the save.
938
+ * app.flow.run().catch((error: unknown) => {
939
+ * if (!(error instanceof SaveUnreadableError)) throw error;
940
+ * showUpdateScreen(error.savedVersion, error.schemaVersion); // 3, 2
941
+ * });
942
+ * ```
943
+ */
944
+ declare class SaveUnreadableError extends Error {
945
+ /** Version found in the save. */
946
+ readonly savedVersion: number;
947
+ /** Version this build writes. */
948
+ readonly schemaVersion: number;
949
+ /**
950
+ * Creates the error.
951
+ *
952
+ * @param savedVersion - Version found in the save.
953
+ * @param schemaVersion - Version this build writes.
954
+ * @param cause - The underlying failure.
955
+ * @example
956
+ * ```ts
957
+ * const error = new SaveUnreadableError(3, 2, new Error("Written by a newer build."));
958
+ * error.savedVersion; // 3
959
+ * ```
960
+ */
961
+ constructor(savedVersion: number, schemaVersion: number, cause: unknown);
962
+ }
963
+ declare namespace types_d_exports$2 {
964
+ export { Api$2 as Api, CommitResult, Config$2 as Config, Events$1 as Events, Json, Migration, ModelCtx, Patch, PlayerStateProvider, ProviderCall, RngApi, RngState, RngStream, RngView, Root, SaveDoc, Snapshot, State$2 as State, StoreApi, Transaction };
965
+ }
966
+ /**
967
+ * Plain JSON value. Everything in state is JSON.
968
+ *
969
+ * @example
970
+ * ```ts
971
+ * const player: Json = { coins: 10, inventory: ["key"] };
972
+ * ```
973
+ */
974
+ type Json = null | boolean | number | string | Json[] | {
975
+ [key: string]: Json;
976
+ };
977
+ /**
978
+ * State roots reported by `model:committed`.
979
+ *
980
+ * @example
981
+ * ```ts
982
+ * const roots: Root[] = ["player", "rng"];
983
+ * ```
984
+ */
985
+ type Root = "player" | "session" | "rng";
986
+ /**
987
+ * model plugin events.
988
+ *
989
+ * @example
990
+ * ```ts
991
+ * // One node run changed the coins and drew from a stream.
992
+ * const payload: Events["model:committed"] = { roots: ["player", "rng"], cause: "edge" };
993
+ * ```
994
+ */
995
+ type Events$1 = {
996
+ /** Committed state changed. Projections reconcile from the snapshot. */"model:committed": {
997
+ roots: readonly Root[];
998
+ cause: "edge" | "rollback" | "restore" | "load";
999
+ };
1000
+ };
1001
+ /**
1002
+ * model plugin config.
1003
+ *
1004
+ * @example
1005
+ * ```ts
1006
+ * createApp({ pluginConfigs: { model: { initialPlayer: { coins: 0 }, seed: 42 } } });
1007
+ * ```
1008
+ */
1009
+ type Config$2 = {
1010
+ /** The save seam. `undefined`: an in-memory provider, nothing is persisted. */playerProvider: PlayerStateProvider | undefined; /** Player state of a new player. Deep-cloned. */
1011
+ initialPlayer: Json; /** Session state at every start. Deep-cloned. */
1012
+ initialSession: Json; /** "from-save": a new player gets a random seed once; a number fixes it (tests). */
1013
+ seed: "from-save" | number; /** Version of the save schema written by this build. */
1014
+ schemaVersion: number; /** Ordered chain; `up` of `from: n` produces version `n + 1`. */
1015
+ migrations: readonly Migration[];
1016
+ };
1017
+ /**
1018
+ * model plugin state: one branch per module.
1019
+ *
1020
+ */
1021
+ type State$2 = {
1022
+ store: StoreState;
1023
+ rng: Record<string, never>;
1024
+ };
1025
+ /**
1026
+ * model plugin API, `app.model`, grouped by module.
1027
+ *
1028
+ * @example
1029
+ * ```ts
1030
+ * app.model.store.snapshot().player; // { coins: 4 }
1031
+ * app.model.rng.peek("chest:42"); // undefined: this chest was never opened
1032
+ * ```
1033
+ */
1034
+ type Api$2 = {
1035
+ store: StoreApi;
1036
+ rng: RngApi;
1037
+ };
1038
+ /**
1039
+ * Domain context shared by the modules. `emit` is the kernel's: `index.ts` writes `events` with
1040
+ * an annotated `register` (core spec `14` row 8), so the own event reaches a factory passed by
1041
+ * direct reference.
1042
+ */
1043
+ type ModelCtx = PluginCtx<Config$2, State$2, Events$1> & {
1044
+ readonly global: object;
1045
+ readonly log: Log.LogApi;
1046
+ };
1047
+ declare namespace types_d_exports$1 {
1048
+ export { Api$1 as Api, Config$1 as Config, FrameCallback, Phase, State$1 as State, Time, TimeCtx };
1049
+ }
1050
+ /**
1051
+ * Frame phases, in call order.
1052
+ *
1053
+ * @example
1054
+ * ```ts
1055
+ * const phase: Phase = "animate";
1056
+ * ```
1057
+ */
1058
+ type Phase = "input" | "animate" | "layout" | "sync" | "signals" | "render";
1059
+ /**
1060
+ * The Time resource, in scaled milliseconds. `idle` tells whether the loop currently runs at the
1061
+ * lowered idle cap.
1062
+ *
1063
+ * @example
1064
+ * ```ts
1065
+ * const time: Time = { delta: 16, elapsed: 1600, scale: 1, frame: 100, idle: false };
1066
+ * ```
1067
+ */
1068
+ type Time = {
1069
+ delta: number;
1070
+ elapsed: number;
1071
+ scale: number;
1072
+ frame: number;
1073
+ idle: boolean;
1074
+ };
1075
+ /**
1076
+ * Callback run once per frame in its phase.
1077
+ *
1078
+ * @example
1079
+ * ```ts
1080
+ * const advanceTweens: FrameCallback = time => tweens.advance(time.delta);
1081
+ * ```
1082
+ */
1083
+ type FrameCallback = (time: Readonly<Time>) => void;
1084
+ /**
1085
+ * time plugin config.
1086
+ *
1087
+ * @example
1088
+ * ```ts
1089
+ * createApp({ pluginConfigs: { time: { maxFps: 30 } } });
1090
+ * ```
1091
+ */
1092
+ type Config$1 = {
1093
+ /**
1094
+ * Frame rate cap. The default is 60. 120 is for WebViews that really deliver 120 Hz frames:
1095
+ * Android today; WKWebView on iOS and macOS is capped at 60 by WebKit (bug 294338).
1096
+ */
1097
+ maxFps: 30 | 60 | 120;
1098
+ /**
1099
+ * Upper bound of one frame's delta in milliseconds.
1100
+ */
1101
+ maxDeltaMs: number;
1102
+ /**
1103
+ * Frame rate cap of an idle screen, when nothing woke the clock for `idleAfterMs`. The default
1104
+ * is 30; 0 turns the idle cap off and every frame runs at `maxFps`.
1105
+ */
1106
+ idleFps: 0 | 30;
1107
+ /**
1108
+ * Unscaled milliseconds without a `wake()` after which the loop drops to `idleFps`.
1109
+ */
1110
+ idleAfterMs: number;
1111
+ };
1112
+ /**
1113
+ * time plugin state.
1114
+ */
1115
+ type State$1 = {
1116
+ callbacks: Record<Phase, FrameCallback[]>;
1117
+ /**
1118
+ * Scratch array of a frame: the six callback lists as they were when the frame started.
1119
+ */
1120
+ captured: (readonly FrameCallback[])[];
1121
+ time: Time;
1122
+ paused: boolean;
1123
+ running: boolean;
1124
+ /**
1125
+ * True while a frame runs; guards `step` re-entry.
1126
+ */
1127
+ stepping: boolean;
1128
+ rafId: number | undefined;
1129
+ lastTimestamp: number | undefined;
1130
+ /**
1131
+ * Elapsed time of the frame source in unscaled milliseconds, the clock of the idle timer: it
1132
+ * keeps running while `scale` is 0.
1133
+ */
1134
+ unscaledElapsedMs: number;
1135
+ /**
1136
+ * Unscaled elapsed time of the last wake.
1137
+ */
1138
+ lastWakeMs: number;
1139
+ /**
1140
+ * True while the loop runs at `idleFps`.
1141
+ */
1142
+ idle: boolean;
1143
+ };
1144
+ /**
1145
+ * time plugin API, `app.time`. The one clock of the screen: frame callbacks per phase, the `Time`
1146
+ * resource, the time scale, and `step` for tests and tools.
1147
+ *
1148
+ * @example
1149
+ * ```ts
1150
+ * // Frame work is a callback in a phase. A test has no frame source: it drives the frames itself.
1151
+ * const off = app.time.onFrame("animate", time => counter.advance(time.delta));
1152
+ *
1153
+ * app.time.step(16);
1154
+ * // the callback gets { delta: 16, elapsed: 16, scale: 1, frame: 1, idle: false }
1155
+ * off();
1156
+ * ```
1157
+ */
1158
+ type Api$1 = {
1159
+ /**
1160
+ * Registers a frame callback. Callbacks of one phase run in registration order; a callback
1161
+ * registered during a frame runs from the next frame on.
1162
+ *
1163
+ * @param phase - Phase the callback belongs to.
1164
+ * @param callback - Function called once per frame with the current `Time`.
1165
+ * @returns Unsubscribe function; calling it twice is a no-op.
1166
+ * @example
1167
+ * ```ts
1168
+ * // The coin counter rolls up while the reward popup is open.
1169
+ * const off = app.time.onFrame("animate", time => counter.advance(time.delta));
1170
+ *
1171
+ * off(); // the popup is closed
1172
+ * ```
1173
+ */
1174
+ onFrame(phase: Phase, callback: FrameCallback): () => void;
1175
+ /**
1176
+ * Returns a snapshot of the current `Time`, so a caller cannot write into the frame state.
1177
+ *
1178
+ * @returns A copy of the current `Time`.
1179
+ * @example
1180
+ * ```ts
1181
+ * // Stamp the start of a combo window in game time, which stands still during a pause.
1182
+ * const startedAt = app.time.snapshot().elapsed; // 1600
1183
+ * app.time.snapshot(); // { delta: 16, elapsed: 1600, scale: 1, frame: 100, idle: false }
1184
+ * ```
1185
+ */
1186
+ snapshot(): Readonly<Time>;
1187
+ /**
1188
+ * Sets the time scale. Every frame delta is multiplied by it; 0 freezes the game time
1189
+ * while the frames keep running. A negative scale is clamped to 0.
1190
+ *
1191
+ * @param scale - New time scale, 0 or greater.
1192
+ * @example
1193
+ * ```ts
1194
+ * // Slow motion while the last match of the level resolves.
1195
+ * app.time.setScale(0.5); // a frame of 20 ms now has delta 10
1196
+ * app.time.setScale(1);
1197
+ * ```
1198
+ */
1199
+ setScale(scale: number): void;
1200
+ /**
1201
+ * Pauses the clock: no phase runs and `elapsed` stops advancing. Called by `lifecycle`.
1202
+ *
1203
+ * @example
1204
+ * ```ts
1205
+ * // The lifecycle plugin owns the pause policy: the first pause reason stops the clock.
1206
+ * // A game pauses through app.lifecycle.push(reason), never here.
1207
+ * const time = ctx.require(timePlugin);
1208
+ * if (reasons.length === 1) time.pause(); // app.time.isPaused() is true, no phase runs
1209
+ * ```
1210
+ */
1211
+ pause(): void;
1212
+ /**
1213
+ * Resumes the clock and drops the stale timestamp, so the first frame after the pause has
1214
+ * a normal delta instead of the whole pause. Called by `lifecycle`.
1215
+ *
1216
+ * @example
1217
+ * ```ts
1218
+ * // The lifecycle plugin resumes when the last pause reason is gone.
1219
+ * const time = ctx.require(timePlugin);
1220
+ * if (reasons.length === 0) time.resume(); // the next frame has a normal delta, not the whole pause
1221
+ * ```
1222
+ */
1223
+ resume(): void;
1224
+ /**
1225
+ * Tells whether the clock is paused.
1226
+ *
1227
+ * @returns True while paused.
1228
+ * @example
1229
+ * ```ts
1230
+ * // A test checks that a pause reason really stopped the frames.
1231
+ * app.lifecycle.push("background");
1232
+ * app.time.isPaused(); // true
1233
+ * ```
1234
+ */
1235
+ isPaused(): boolean;
1236
+ /**
1237
+ * Tells whether a real frame source drives the loop. False in plain Bun, where a test
1238
+ * drives the frames with `step`.
1239
+ *
1240
+ * @returns True while the loop runs.
1241
+ * @example
1242
+ * ```ts
1243
+ * // Without a frame source no frame ever comes: finish the fly-in at once instead of waiting.
1244
+ * if (!app.time.isRunning()) coin.moveTo(target); // false in plain Bun, true in a browser
1245
+ * ```
1246
+ */
1247
+ isRunning(): boolean;
1248
+ /**
1249
+ * Resets the idle timer: the next frame runs at `maxFps` again, even after a long idle. Cheap,
1250
+ * idempotent and safe inside a frame callback. Every plugin that gives the player something to
1251
+ * look at calls it: a pointer sample, a starting animation, a flow edge, a finished load, a
1252
+ * scene switch, a locale change, a reconcile.
1253
+ *
1254
+ * @example
1255
+ * ```ts
1256
+ * // The flow runner walked an edge, so the screen has work again and the cap goes back up.
1257
+ * const time = ctx.require(timePlugin);
1258
+ *
1259
+ * time.wake(); // app.time.snapshot().idle is false from here on
1260
+ * ```
1261
+ */
1262
+ wake(): void;
1263
+ /**
1264
+ * Runs exactly one frame with the given unscaled delta, ignoring the fps cap, the pause
1265
+ * flag and `maxDeltaMs`. The time scale still applies. For tests and tools.
1266
+ *
1267
+ * @param deltaMs - Unscaled delta of the frame in milliseconds.
1268
+ * @throws {Error} When called from inside a frame callback.
1269
+ * @example
1270
+ * ```ts
1271
+ * // A test plays two frames without a browser.
1272
+ * app.time.step(16);
1273
+ * app.time.step(4);
1274
+ * app.time.snapshot(); // { delta: 4, elapsed: 20, scale: 1, frame: 2, idle: false }
1275
+ * ```
1276
+ */
1277
+ step(deltaMs: number): void;
1278
+ };
1279
+ /**
1280
+ * Domain context of the time plugin.
1281
+ */
1282
+ type TimeCtx = PluginCtx<Config$1, State$1> & {
1283
+ readonly global: object;
1284
+ readonly log: Log.LogApi;
1285
+ };
1286
+ //#endregion
1287
+ //#region src/plugins/flow/gate/types.d.ts
1288
+ /**
1289
+ * One answer of the player, of an agent or of a test.
1290
+ *
1291
+ * @example
1292
+ * ```ts
1293
+ * const answer: Answer = { intent: "merge", payload: { from: "c2", to: "c3" } };
1294
+ * ```
1295
+ */
1296
+ type Answer = {
1297
+ intent: string;
1298
+ payload?: Json;
1299
+ };
1300
+ /**
1301
+ * The one answer a `guide` lets through. `payload` is compared by deep equality when present.
1302
+ *
1303
+ * @example
1304
+ * ```ts
1305
+ * const allow: Allow = { intent: "merge", payload: { from: "c2", to: "c3" } };
1306
+ * ```
1307
+ */
1308
+ type Allow = {
1309
+ intent: string;
1310
+ payload?: Json;
1311
+ };
1312
+ /**
1313
+ * What an open gate accepts.
1314
+ */
1315
+ type GateSpec = {
1316
+ allowed: readonly string[];
1317
+ };
1318
+ /**
1319
+ * gate module state.
1320
+ */
1321
+ type GateState = {
1322
+ /** The spec of the open gate. `undefined`: the gate is closed. */open: GateSpec | undefined; /** Resolver of the promise returned by `open`. */
1323
+ resolve: ((answer: Answer) => void) | undefined; /** An answer given while the gate was closed, kept for one frame. */
1324
+ held: {
1325
+ answer: Answer;
1326
+ frame: number;
1327
+ } | undefined; /** The one answer a running `guide` lets through. */
1328
+ narrow: Allow | undefined; /** True while a pointer is down. Entering an `over` node waits for false. */
1329
+ pointerActive: boolean; /** Ends the pending pointer wait of the runner. Set while the wait runs, so a stop needs no frame. */
1330
+ wake: (() => void) | undefined;
1331
+ };
1332
+ /**
1333
+ * gate module API, `app.flow.gate`: the single entry of player answers.
1334
+ *
1335
+ * @example
1336
+ * ```ts
1337
+ * // The screen turns a tap into an answer. A double tap is safe: the gate closes before the
1338
+ * // first answer is handed on.
1339
+ * app.flow.gate.answer({ intent: "play" }); // true: "home" rests and lists "play"
1340
+ * app.flow.gate.answer({ intent: "play" }); // false: the second tap hits a closed door
1341
+ * ```
1342
+ */
1343
+ type GateApi = {
1344
+ /**
1345
+ * Gives one answer. Accepted only while the gate is open and the intent is allowed. An answer
1346
+ * to a closed gate is held for one frame and re-offered when the gate opens within it.
1347
+ *
1348
+ * @param answer - Intent and optional payload.
1349
+ * @returns Whether the answer was accepted.
1350
+ * @example
1351
+ * ```ts
1352
+ * // The player dropped the item of cell c2 onto c3 while the board rests.
1353
+ * app.flow.gate.answer({ intent: "merge", payload: { from: "c2", to: "c3" } }); // true
1354
+ * app.flow.gate.answer({ intent: "quit" }); // false: the resting node does not list "quit"
1355
+ * ```
1356
+ */
1357
+ answer(answer: Answer): boolean;
1358
+ /**
1359
+ * Records that a pointer is down. While true the runner delays entering an `over` node, so a
1360
+ * popup never appears mid-drag.
1361
+ *
1362
+ * @param active - True while the pointer is down.
1363
+ * @example
1364
+ * ```ts
1365
+ * // The input layer reports a drag on the board.
1366
+ * app.flow.gate.pointer(true); // pointer down: an `over` node that comes next waits
1367
+ * app.flow.gate.pointer(false); // pointer up: the node is entered in the next `signals` phase
1368
+ * ```
1369
+ */
1370
+ pointer(active: boolean): void;
1371
+ /**
1372
+ * Reads what the gate takes right now.
1373
+ *
1374
+ * @returns Whether the gate is open, the allowed intents and whether a `guide` narrows it.
1375
+ * @example
1376
+ * ```ts
1377
+ * // The screen greys out a button whose intent the resting node does not take.
1378
+ * app.flow.gate.state(); // { open: true, allowed: ["play", "shop"], narrowed: false }
1379
+ * ```
1380
+ */
1381
+ state(): {
1382
+ open: boolean;
1383
+ allowed: readonly string[];
1384
+ narrowed: boolean;
1385
+ };
1386
+ };
1387
+ //#endregion
1388
+ //#region src/plugins/flow/fx/types.d.ts
1389
+ /**
1390
+ * An effect a node awaits. With `answers` the runner opens the gate for those intents.
1391
+ * `cosmetic: true` swallows a handler error and resolves `undefined`.
1392
+ *
1393
+ * @example
1394
+ * ```ts
1395
+ * const descriptor: Descriptor = { kind: "popup", payload: { name: "Retry" }, answers: ["again"] };
1396
+ * ```
1397
+ */
1398
+ type Descriptor = {
1399
+ kind: string;
1400
+ payload?: Json;
1401
+ answers?: readonly string[];
1402
+ cosmetic?: boolean;
1403
+ };
1404
+ /**
1405
+ * A fire-and-forget cosmetic effect. Released only after the commit of its transaction.
1406
+ *
1407
+ * @example
1408
+ * ```ts
1409
+ * const sparkle: Hint = { kind: "sparkle", payload: { cell: "c3" }, hint: true };
1410
+ * ```
1411
+ */
1412
+ type Hint = {
1413
+ kind: string;
1414
+ payload?: Json;
1415
+ hint: true;
1416
+ };
1417
+ /**
1418
+ * Options of the `guide` descriptor: the one allowed answer and its visual part. `target` names
1419
+ * the keyed element the visual hole of `ui` is cut over.
1420
+ *
1421
+ * @example
1422
+ * ```ts
1423
+ * const options: GuideOptions = {
1424
+ * allow: { intent: "merge" },
1425
+ * target: { projection: "board", key: "c3" },
1426
+ * hand: "drag"
1427
+ * };
1428
+ * ```
1429
+ */
1430
+ type GuideOptions = {
1431
+ allow: Allow;
1432
+ target?: {
1433
+ projection: string;
1434
+ key: string;
1435
+ };
1436
+ highlight?: readonly string[];
1437
+ hand?: "tap" | "drag";
1438
+ text?: string;
1439
+ };
1440
+ /**
1441
+ * Handler of one effect kind, registered by a plugin above `flow`. `signal` is the node's abort
1442
+ * signal, except for a descriptor with `answers` and for `guide`: those get a signal of their own,
1443
+ * aborted when the answer arrives or the narrow is lifted, so the handler unmounts what it showed.
1444
+ *
1445
+ * @example
1446
+ * ```ts
1447
+ * // The handler of "sfx": start the sound the node named, stop it when the node is aborted.
1448
+ * const playSound: FxHandler = (descriptor, { signal }) => {
1449
+ * const sound = sounds.play(descriptor.payload); // payload: { name: "merge" }
1450
+ *
1451
+ * signal.addEventListener("abort", () => sound.stop());
1452
+ * };
1453
+ *
1454
+ * app.flow.fx.handle("sfx", playSound);
1455
+ * ```
1456
+ */
1457
+ type FxHandler = (descriptor: Descriptor | Hint, ctx: {
1458
+ signal: AbortSignal;
1459
+ mode: "live" | "fast";
1460
+ }) => unknown | Promise<unknown>;
1461
+ /**
1462
+ * Listener of released hints, registered with `fx.onHint`. Every listener hears every hint, so it
1463
+ * reads `kind` itself.
1464
+ *
1465
+ * @example
1466
+ * ```ts
1467
+ * // The world plugin plays a motion for the hints it knows and ignores the rest.
1468
+ * const playMotion: HintListener = released => {
1469
+ * if (released.kind === "merged") playMergeInto(released.payload);
1470
+ * };
1471
+ * ```
1472
+ */
1473
+ type HintListener = (hint: Hint) => void;
1474
+ /**
1475
+ * fx module state.
1476
+ */
1477
+ type FxState = {
1478
+ /** One handler per effect kind. */handlers: Map<string, {
1479
+ run: FxHandler;
1480
+ runInFast: boolean;
1481
+ }>; /** Hints of the open transaction. */
1482
+ buffered: Hint[]; /** Listeners of released hints, in registration order. */
1483
+ hintListeners: HintListener[]; /** Completions waiting for the `signals` phase, in start order. */
1484
+ settled: Array<() => void>; /** The child controllers of the `guide` handlers that are showing something right now. */
1485
+ guides: AbortController[];
1486
+ mode: "live" | "fast";
1487
+ };
1488
+ /**
1489
+ * fx module API, `app.flow.fx`: the effects gateway.
1490
+ *
1491
+ * @example
1492
+ * ```ts
1493
+ * // The popup layer owns the kind "popup". It shows the UI; the answer comes through the gate.
1494
+ * app.flow.fx.handle("popup", descriptor => showPopup(descriptor.payload));
1495
+ *
1496
+ * // A node then writes: await fx({ kind: "popup", answers: ["close"] });
1497
+ * ```
1498
+ */
1499
+ type FxApi = {
1500
+ /**
1501
+ * Registers the single handler of one effect kind. A second handler for the same kind throws:
1502
+ * an effect has exactly one owner.
1503
+ *
1504
+ * @param kind - Effect kind, for example `"sfx"`.
1505
+ * @param handler - Called with the descriptor and `{ signal, mode }`.
1506
+ * @param options - Registration options.
1507
+ * @param options.runInFast - `true` makes the handler run in fast mode too.
1508
+ * @returns The unregister function. It removes only the handler it registered.
1509
+ * @throws {Error} When the kind already has a handler.
1510
+ * @example
1511
+ * ```ts
1512
+ * // Preloading is real work, so it runs in a fast walk too. Sounds and popups do not.
1513
+ * const off = app.flow.fx.handle("load", preload, { runInFast: true });
1514
+ *
1515
+ * off(); // "load" has no handler again: the kind is free for another owner
1516
+ * ```
1517
+ */
1518
+ handle(kind: string, handler: FxHandler, options?: {
1519
+ runInFast?: boolean;
1520
+ }): () => void;
1521
+ /**
1522
+ * Delivers a descriptor or a released hint to its handler and forgets about it. In fast mode
1523
+ * only a handler registered with `runInFast` is called, exactly as for an awaited effect. A
1524
+ * missing handler is not an error and a failing handler is logged, never thrown.
1525
+ *
1526
+ * @param descriptor - The descriptor or hint to deliver.
1527
+ * @example
1528
+ * ```ts
1529
+ * // The screen wants a haptic tick outside any node. Nothing waits for it.
1530
+ * app.flow.fx.dispatch({ kind: "haptic", payload: { style: "light" } });
1531
+ * ```
1532
+ */
1533
+ dispatch(descriptor: Descriptor | Hint): void;
1534
+ /**
1535
+ * Registers a listener for every released hint. A hint reaches it after the commit of its edge,
1536
+ * in the order the node emitted them; a discarded transaction sends nothing, and fast mode drops
1537
+ * hints altogether. The handler `handle` registered for the same kind is served as well: a
1538
+ * listener displaces nobody.
1539
+ *
1540
+ * @param listener - Called with each released hint.
1541
+ * @returns The unregister function. It removes only the listener it registered.
1542
+ * @example
1543
+ * ```ts
1544
+ * // The world plugin turns the hints of a committed edge into projection motions.
1545
+ * const off = app.flow.fx.onHint(hint => {
1546
+ * if (hint.kind === "merged") playMergeInto(hint.payload); // payload: { from: "c1", to: "c2" }
1547
+ * });
1548
+ *
1549
+ * off(); // the plugin stopped: no hint reaches it any more
1550
+ * ```
1551
+ */
1552
+ onHint(listener: HintListener): () => void;
1553
+ };
1554
+ /**
1555
+ * The `fx` of a node context: awaited effects by call, cosmetic hints by `emit`.
1556
+ *
1557
+ * @example
1558
+ * ```ts
1559
+ * // Inside a node body: wait for the player's pick, then sparkle once the edge has committed.
1560
+ * const pick = await fx({ kind: "popup", payload: { name: "Retry" }, answers: ["again", "home"] });
1561
+ * // pick: { intent: "again" }
1562
+ * fx.emit(hint("sparkle", { cell: "c3" }));
1563
+ * ```
1564
+ */
1565
+ type NodeFx = ((descriptor: Descriptor) => Promise<unknown>) & {
1566
+ emit(hint: Hint): void;
1567
+ };
1568
+ //#endregion
1569
+ //#region src/plugins/flow/inbox/types.d.ts
1570
+ /**
1571
+ * One event of the world: time that passed, a push message, a purchase that arrived.
1572
+ *
1573
+ * @example
1574
+ * ```ts
1575
+ * const event: WorldEvent = { type: "elapsed", payload: { now: 1000 } };
1576
+ * ```
1577
+ */
1578
+ type WorldEvent = {
1579
+ type: string;
1580
+ payload?: Json;
1581
+ };
1582
+ /**
1583
+ * inbox module state.
1584
+ */
1585
+ type InboxState = {
1586
+ /** Undelivered events, oldest first. One entry per `type`. */queue: WorldEvent[]; /** Callbacks woken by every `post`. Kept in state so every API instance over this state shares them. */
1587
+ listeners: Array<() => void>;
1588
+ };
1589
+ /**
1590
+ * inbox module API, `app.flow.inbox`: world events wait here until a rest node that lists their
1591
+ * type is active.
1592
+ *
1593
+ * @example
1594
+ * ```ts
1595
+ * // The shop SDK confirms a purchase at any moment. The graph takes it at the next rest node
1596
+ * // that declares `inbox: ["purchased"]`, as the outcome "purchased" of that node.
1597
+ * app.flow.inbox.post({ type: "purchased", payload: { sku: "coins100" } });
1598
+ * ```
1599
+ */
1600
+ type InboxApi = {
1601
+ /**
1602
+ * Queues one world event. It is delivered only when the active node is a rest node whose
1603
+ * `inbox` lists the type; until then it waits. Duplicates of one type collapse to the latest.
1604
+ *
1605
+ * @param event - Type and optional payload.
1606
+ * @example
1607
+ * ```ts
1608
+ * // Two push messages arrive while a transit node runs. The next rest node gets one event.
1609
+ * app.flow.inbox.post({ type: "gift", payload: { coins: 50 } });
1610
+ * app.flow.inbox.post({ type: "gift", payload: { coins: 75 } }); // delivered: { coins: 75 }
1611
+ * ```
1612
+ */
1613
+ post(event: WorldEvent): void;
1614
+ };
1615
+ //#endregion
1616
+ //#region src/plugins/flow/runner/types.d.ts
1617
+ /**
1618
+ * A value that carries only a type. Invariant in `Payload`, so `{ stars }` never passes for
1619
+ * `{ stars, moves }` in either direction.
1620
+ *
1621
+ * @example
1622
+ * ```ts
1623
+ * const stars: TypeTag<{ stars: number }> = type<{ stars: number }>();
1624
+ * ```
1625
+ */
1626
+ type TypeTag<Payload> = {
1627
+ readonly kind: "type";
1628
+ readonly $type?: (value: Payload) => Payload;
1629
+ };
1630
+ /**
1631
+ * Any type tag. The bound of tag records: it names no payload, so `type()` infers nothing from it.
1632
+ */
1633
+ type AnyTypeTag = {
1634
+ readonly kind: "type";
1635
+ };
1636
+ /**
1637
+ * Outcome name to the type tag of its payload.
1638
+ *
1639
+ * @example
1640
+ * ```ts
1641
+ * const outcomes = { done: type(), failed: type<{ reason: string }>() } satisfies OutcomeTags;
1642
+ * ```
1643
+ */
1644
+ type OutcomeTags = Readonly<Record<string, AnyTypeTag>>;
1645
+ /**
1646
+ * The payload type carried by a type tag.
1647
+ *
1648
+ * @example
1649
+ * ```ts
1650
+ * type Reason = PayloadOf<TypeTag<{ reason: string }>>; // { reason: string }
1651
+ * ```
1652
+ */
1653
+ type PayloadOf<Tag> = Tag extends TypeTag<infer Payload> ? Payload : never;
1654
+ /**
1655
+ * The part of the game types a node sees. Inside the engine both trees are `Json`;
1656
+ * `defineGame<Types>()` narrows them for the game at the type level only.
1657
+ *
1658
+ * @example
1659
+ * ```ts
1660
+ * const game: GameState = { player: { lives: 3 }, session: { visits: 0 } };
1661
+ * ```
1662
+ */
1663
+ type GameState = {
1664
+ player: Json;
1665
+ session: Json;
1666
+ scenes?: string;
1667
+ };
1668
+ /**
1669
+ * The scene ids a game declared, `string` when it declared none.
1670
+ *
1671
+ * @example
1672
+ * ```ts
1673
+ * type Ids = SceneIdOf<{ player: {}; session: {}; scenes: "home" | "board" }>; // "home" | "board"
1674
+ * ```
1675
+ */
1676
+ type SceneIdOf<Game extends GameState> = Game extends {
1677
+ scenes: infer Ids extends string;
1678
+ } ? Ids : string;
1679
+ /**
1680
+ * Whether a payload or input type is `void`: an outcome without data, a node without input.
1681
+ *
1682
+ * @example
1683
+ * ```ts
1684
+ * type Empty = IsVoid<PayloadOf<TypeTag<void>>>; // true
1685
+ * ```
1686
+ */
1687
+ type IsVoid<Payload> = [Payload] extends [void] ? true : false;
1688
+ /**
1689
+ * What `out.name(data)` returns. `run` must return one of these; a bare string is not accepted.
1690
+ * `Name` is the union of the node's outcome names; without a type argument it is the widened
1691
+ * result the runner reads.
1692
+ *
1693
+ * @example
1694
+ * ```ts
1695
+ * // What `out.rejected({ reason: "empty" })` returns, and what the journal keeps of the edge.
1696
+ * const result: Result = { outcome: "rejected", payload: { reason: "empty" } };
1697
+ * ```
1698
+ */
1699
+ type Result<Name extends string = string> = {
1700
+ readonly outcome: Name;
1701
+ readonly payload: Json;
1702
+ };
1703
+ /**
1704
+ * The `out` of a node context: one method per declared outcome, without an argument when the
1705
+ * payload is `void`.
1706
+ *
1707
+ * @example
1708
+ * ```ts
1709
+ * // In a node with outcomes { done: type(), rejected: type<{ reason: string }>() }:
1710
+ * out.done(); // { outcome: "done", payload: null }
1711
+ * out.rejected({ reason: "empty" }); // { outcome: "rejected", payload: { reason: "empty" } }
1712
+ * ```
1713
+ */
1714
+ type Out<Tags extends OutcomeTags> = { readonly [Name in keyof Tags]: IsVoid<PayloadOf<Tags[Name]>> extends true ? () => Result<keyof Tags & string> : (data: PayloadOf<Tags[Name]>) => Result<keyof Tags & string> };
1715
+ /**
1716
+ * The one object a node body receives. `player` and `session` are drafts of the open
1717
+ * transaction, `rng` is its view, `now` is `clock.now()` read once at node entry.
1718
+ *
1719
+ * @example
1720
+ * ```ts
1721
+ * // The "roll" node of a dice game: the drafts are written in place, the edge commits them.
1722
+ * run: ({ player, session, rng, out }) => {
1723
+ * player.coins += rng.stream("dice").range(1, 6);
1724
+ * session.rolls += 1;
1725
+ * return out.done();
1726
+ * }
1727
+ * ```
1728
+ */
1729
+ type NodeContext<Game extends GameState = GameState, Input = void, Tags extends OutcomeTags = OutcomeTags> = {
1730
+ input: Input;
1731
+ player: Game["player"];
1732
+ session: Game["session"];
1733
+ rng: RngView;
1734
+ fx: NodeFx;
1735
+ out: Out<Tags>;
1736
+ signal: AbortSignal;
1737
+ now: number;
1738
+ };
1739
+ /**
1740
+ * The body of a node: plain `await` code that ends with `out.name(data)`.
1741
+ */
1742
+ type NodeRun<Game extends GameState, Input, Tags extends OutcomeTags> = (ctx: NodeContext<Game, Input, Tags>) => Result<keyof Tags & string> | Promise<Result<keyof Tags & string>>;
1743
+ /**
1744
+ * The part of a node, or of a flow used as a node, that an edge table looks at.
1745
+ */
1746
+ type Wired<Input, Tags extends OutcomeTags> = {
1747
+ readonly input: TypeTag<Input>;
1748
+ readonly outcomes: Tags;
1749
+ };
1750
+ /**
1751
+ * Anything that can sit in the `nodes` of a flow: a node, a sub-flow or a slot.
1752
+ */
1753
+ type AnyWired = {
1754
+ readonly input: AnyTypeTag;
1755
+ readonly outcomes: OutcomeTags;
1756
+ };
1757
+ /**
1758
+ * Node name to node: the `nodes` of one flow.
1759
+ */
1760
+ type NodeTable = Readonly<Record<string, AnyWired>>;
1761
+ /**
1762
+ * A node as `defineNode` returns it: plain data plus the optional body.
1763
+ *
1764
+ * @example
1765
+ * ```ts
1766
+ * // What defineNode({ outcomes: { play: type() }, rest: true, checkpoint: true }) returns:
1767
+ * // { kind: "node", input: { kind: "type" }, outcomes: { play: { kind: "type" } }, rest: true,
1768
+ * // over: false, checkpoint: true, barrier: false, inbox: [] }
1769
+ * ```
1770
+ */
1771
+ type NodeDefinition<Input, Tags extends OutcomeTags, Game extends GameState = GameState> = Wired<Input, Tags> & {
1772
+ readonly kind: "node";
1773
+ readonly rest: boolean;
1774
+ readonly over: boolean;
1775
+ readonly checkpoint: boolean;
1776
+ readonly barrier: boolean;
1777
+ readonly inbox: readonly (keyof Tags & string)[]; /** Id of the scene the node is shown on. Absent: the node keeps the current scene. */
1778
+ readonly scene?: string;
1779
+ readonly run?: NodeRun<Game, Input, Tags>;
1780
+ };
1781
+ /**
1782
+ * What the author passes to `defineNode`. `run` is required unless `rest: true`: a rest node
1783
+ * without a body is a pure wait.
1784
+ *
1785
+ * @example
1786
+ * ```ts
1787
+ * const spec: NodeSpec<Game, void, { play: TypeTag<void> }> = {
1788
+ * rest: true,
1789
+ * checkpoint: true,
1790
+ * scene: "home",
1791
+ * outcomes: { play: type() }
1792
+ * };
1793
+ * ```
1794
+ */
1795
+ type NodeSpec<Game extends GameState, Input, Tags extends OutcomeTags> = {
1796
+ input?: TypeTag<Input>;
1797
+ outcomes: Tags;
1798
+ over?: boolean;
1799
+ checkpoint?: boolean;
1800
+ barrier?: boolean;
1801
+ inbox?: readonly (keyof Tags & string)[]; /** Id of the scene this node is shown on. Without it the node keeps the current scene. */
1802
+ scene?: SceneIdOf<Game>;
1803
+ } & ({
1804
+ rest: true;
1805
+ run?: NodeRun<Game, Input, Tags>;
1806
+ } | {
1807
+ rest?: false;
1808
+ run: NodeRun<Game, Input, Tags>;
1809
+ });
1810
+ /**
1811
+ * The signature of `defineNode` bound to one game. `defineGame<Types>()` returns it.
1812
+ *
1813
+ * @example
1814
+ * ```ts
1815
+ * const defineGameNode: DefineNode<{ player: Player; session: Session }> = defineNode;
1816
+ * ```
1817
+ */
1818
+ type DefineNode<Game extends GameState> = <Input = void, const Tags extends OutcomeTags = OutcomeTags>(spec: NodeSpec<Game, Input, Tags>) => NodeDefinition<Input, Tags, Game>;
1819
+ /**
1820
+ * An extension point inside a flow: a node whose body is "run the contributions in order".
1821
+ * Its single outcome is `done`.
1822
+ *
1823
+ * @example
1824
+ * ```ts
1825
+ * const afterOrder: SlotNode = slot("afterOrder");
1826
+ * // { kind: "slot", name: "afterOrder", input: { kind: "type" },
1827
+ * // outcomes: { done: { kind: "type" } } }
1828
+ * ```
1829
+ */
1830
+ type SlotNode = Wired<void, {
1831
+ readonly done: TypeTag<void>;
1832
+ }> & {
1833
+ readonly kind: "slot";
1834
+ readonly name: string;
1835
+ };
1836
+ /**
1837
+ * Edge target that leaves the sub-flow with this outcome. The payload passes through.
1838
+ *
1839
+ * @example
1840
+ * ```ts
1841
+ * const leave: Exit<"left"> = exit("left"); // { kind: "exit", outcome: "left" }
1842
+ * ```
1843
+ */
1844
+ type Exit<Name extends string = string> = {
1845
+ readonly kind: "exit";
1846
+ readonly outcome: Name;
1847
+ };
1848
+ /**
1849
+ * Edge target that adapts the payload on the edge. `map` is a property on purpose: the mapper's
1850
+ * parameter annotation is checked strictly against the outcome payload.
1851
+ *
1852
+ * @example
1853
+ * ```ts
1854
+ * const adapt: Mapped<"retry", { reason: string }, { why: string }> = to(
1855
+ * "retry",
1856
+ * (failure: { reason: string }) => ({ why: failure.reason })
1857
+ * );
1858
+ * ```
1859
+ */
1860
+ type Mapped<TargetName extends string = string, Payload = never, Output = unknown> = {
1861
+ readonly kind: "map";
1862
+ readonly target: TargetName;
1863
+ readonly map: (payload: Payload) => Output;
1864
+ };
1865
+ /**
1866
+ * A mapped target as the runner calls it. `map` is a method on purpose: method parameters are
1867
+ * compared bivariantly, so every checked `Mapped` fits and the runner calls it without a cast.
1868
+ * The result is `unknown`: a mapper into a node without input returns nothing, and the runner
1869
+ * checks that a mapped payload is plain JSON before it becomes the next input.
1870
+ */
1871
+ type AnyMapped = {
1872
+ readonly kind: "map";
1873
+ readonly target: string;
1874
+ map(payload: unknown): unknown;
1875
+ };
1876
+ /**
1877
+ * Any edge target as the runner reads it: a node name, an exit or a mapped target.
1878
+ *
1879
+ * @example
1880
+ * ```ts
1881
+ * const next: Target = "home"; // a node of the same flow
1882
+ * const leave: Target = exit("left"); // { kind: "exit", outcome: "left" }
1883
+ * ```
1884
+ */
1885
+ type Target = string | Exit | AnyMapped;
1886
+ /**
1887
+ * Whether a receiver with input `Input` takes `Payload`. A receiver without input ignores the
1888
+ * payload, so it accepts anything; a void outcome into a node with input is an error.
1889
+ *
1890
+ * @example
1891
+ * ```ts
1892
+ * type Fits = Accepts<{ stars: number }, void>; // true
1893
+ * ```
1894
+ */
1895
+ type Accepts<Payload, Input> = IsVoid<Input> extends true ? true : [Payload] extends [Input] ? true : false;
1896
+ /**
1897
+ * The input type of a node or sub-flow.
1898
+ *
1899
+ * @example
1900
+ * ```ts
1901
+ * type MergeInput = InputOf<typeof merge>; // { from: string; to: string }
1902
+ * ```
1903
+ */
1904
+ type InputOf<Node extends AnyWired> = PayloadOf<Node["input"]>;
1905
+ /**
1906
+ * Names of the nodes whose input accepts `Payload`.
1907
+ *
1908
+ * @example
1909
+ * ```ts
1910
+ * type Starts = NodeNamesFor<{ boot: typeof boot; merge: typeof merge }, void>; // "boot"
1911
+ * ```
1912
+ */
1913
+ type NodeNamesFor<Nodes extends NodeTable, Payload> = { [Name in keyof Nodes & string]: Accepts<Payload, InputOf<Nodes[Name]>> extends true ? Name : never }[keyof Nodes & string];
1914
+ /**
1915
+ * Names of the flow outcomes whose payload accepts `Payload`.
1916
+ *
1917
+ * @example
1918
+ * ```ts
1919
+ * type Exits = ExitNamesFor<{ win: TypeTag<{ stars: number }> }, { stars: number }>; // "win"
1920
+ * ```
1921
+ */
1922
+ type ExitNamesFor<FlowTags extends OutcomeTags, Payload> = { [Name in keyof FlowTags & string]: Accepts<Payload, PayloadOf<FlowTags[Name]>> extends true ? Name : never }[keyof FlowTags & string];
1923
+ /**
1924
+ * Every correctly typed `to(node, map)` for `Payload`.
1925
+ */
1926
+ type MappedFor<Nodes extends NodeTable, Payload> = { [Name in keyof Nodes & string]: Mapped<Name, Payload, InputOf<Nodes[Name]>> }[keyof Nodes & string];
1927
+ /**
1928
+ * Descriptive type: every legal target of an outcome that carries `Payload`.
1929
+ */
1930
+ type TargetFor<Nodes extends NodeTable, FlowTags extends OutcomeTags, Payload> = NodeNamesFor<Nodes, Payload> | Exit<ExitNamesFor<FlowTags, Payload>> | MappedFor<Nodes, Payload>;
1931
+ /**
1932
+ * Descriptive type of an edge table: one entry per outcome of every node. Stored on
1933
+ * `FlowDefinition` and read by tools; the check with readable errors is `CheckedEdges`.
1934
+ *
1935
+ * @example
1936
+ * ```ts
1937
+ * const edges: Edges<{ boot: typeof boot; home: typeof home }, Record<never, never>> = {
1938
+ * boot: { ready: "home" },
1939
+ * home: { play: "boot" }
1940
+ * };
1941
+ * ```
1942
+ */
1943
+ type Edges<Nodes extends NodeTable, FlowTags extends OutcomeTags> = { readonly [Name in keyof Nodes]: { readonly [Outcome in keyof Nodes[Name]["outcomes"]]: TargetFor<Nodes, FlowTags, PayloadOf<Nodes[Name]["outcomes"][Outcome]>> } };
1944
+ /**
1945
+ * A branded sentence. The compiler prints it in the "is not assignable to" position, so a wrong
1946
+ * edge reads as a message with node and outcome names.
1947
+ *
1948
+ * @example
1949
+ * ```ts
1950
+ * type Problem = GraphError<'No node "awaitIntnet" in this flow (edge of outcome "done" of node "merge").'>;
1951
+ * ```
1952
+ */
1953
+ type GraphError<Message extends string> = {
1954
+ readonly $graphError: Message;
1955
+ };
1956
+ /**
1957
+ * The loosest legal target of a flow: what a missing entry is expected to be.
1958
+ */
1959
+ type AnyTargetOf<Nodes extends NodeTable, FlowTags extends OutcomeTags> = (keyof Nodes & string) | Exit<keyof FlowTags & string> | Mapped<keyof Nodes & string, never, unknown>;
1960
+ /**
1961
+ * Checks one entry `Value` of the inferred edge table. A legal entry stays itself; a wrong one
1962
+ * becomes a `GraphError` sentence.
1963
+ */
1964
+ type CheckTarget<Nodes extends NodeTable, FlowTags extends OutcomeTags, NodeName extends string, OutcomeName extends string, Payload, Value> = Value extends string ? Value extends keyof Nodes ? Accepts<Payload, InputOf<Nodes[Value]>> extends true ? Value : GraphError<`Payload of outcome "${OutcomeName}" of node "${NodeName}" does not fit the input of node "${Value}". Use to("${Value}", payload => ...).`> : GraphError<`No node "${Value}" in this flow (edge of outcome "${OutcomeName}" of node "${NodeName}").`> : Value extends Exit<infer ExitName> ? ExitName extends keyof FlowTags ? Accepts<Payload, PayloadOf<FlowTags[ExitName]>> extends true ? Value : GraphError<`Payload of outcome "${OutcomeName}" of node "${NodeName}" does not fit flow outcome "${ExitName}".`> : GraphError<`exit("${ExitName}"): the flow declares no outcome "${ExitName}".`> : Value extends Mapped<infer TargetName, never, unknown> ? TargetName extends keyof Nodes ? Mapped<TargetName, Payload, InputOf<Nodes[TargetName]>> : GraphError<`to("${TargetName}", ...): no node "${TargetName}" in this flow.`> : AnyTargetOf<Nodes, FlowTags>;
1965
+ /**
1966
+ * Checking type: validates the inferred edge table `Table` entry by entry. It reports a missing
1967
+ * edge, an unknown target, a payload that does not fit, an edge of an unknown outcome and an
1968
+ * edge table row of an unknown node.
1969
+ *
1970
+ * @example
1971
+ * ```ts
1972
+ * type Spec = { edges: Table & CheckedEdges<Nodes, FlowTags, Table> };
1973
+ * ```
1974
+ */
1975
+ type CheckedEdges<Nodes extends NodeTable, FlowTags extends OutcomeTags, Table> = { readonly [Name in keyof Nodes & string]: { readonly [Outcome in keyof Nodes[Name]["outcomes"] & string]: Name extends keyof Table ? Outcome extends keyof Table[Name] ? CheckTarget<Nodes, FlowTags, Name, Outcome, PayloadOf<Nodes[Name]["outcomes"][Outcome]>, Table[Name][Outcome]> : AnyTargetOf<Nodes, FlowTags> : AnyTargetOf<Nodes, FlowTags> } & (Name extends keyof Table ? { readonly [Extra in Exclude<keyof Table[Name], keyof Nodes[Name]["outcomes"]> & string]: GraphError<`Node "${Name}" has no outcome "${Extra}".`> } : unknown) } & { readonly [Extra in Exclude<keyof Table, keyof Nodes> & string]: GraphError<`"${Extra}" is not a node of this flow.`> };
1976
+ /**
1977
+ * A flow as `defineFlow` returns it: plain data. A flow that declares `input` and `outcomes`
1978
+ * is used as a node of another flow.
1979
+ *
1980
+ * @example
1981
+ * ```ts
1982
+ * // What defineFlow("main", { nodes: { boot, home }, start: "boot", edges }) returns:
1983
+ * // { kind: "flow", id: "main", input: { kind: "type" }, outcomes: {}, nodes: { boot, home },
1984
+ * // start: "boot", edges: { boot: { ready: "home" }, home: { play: "boot" } } }
1985
+ * ```
1986
+ */
1987
+ type FlowDefinition<Input, FlowTags extends OutcomeTags, Nodes extends NodeTable = NodeTable> = Wired<Input, FlowTags> & {
1988
+ readonly kind: "flow";
1989
+ readonly id: string;
1990
+ readonly nodes: Nodes;
1991
+ readonly start: keyof Nodes & string;
1992
+ readonly edges: Edges<Nodes, FlowTags>;
1993
+ };
1994
+ /**
1995
+ * What the author passes to `defineFlow`. `start` must name a node whose input accepts the flow
1996
+ * input; `edges` is inferred as written and validated by `CheckedEdges`.
1997
+ *
1998
+ * @example
1999
+ * ```ts
2000
+ * // The spec of a sub-flow: `outcomes` makes it usable as a node, exit() leaves it.
2001
+ * defineFlow("rewardPopup", {
2002
+ * nodes: { show, grant },
2003
+ * start: "show",
2004
+ * outcomes: { done: type() },
2005
+ * edges: { show: { claim: "grant" }, grant: { done: exit("done") } }
2006
+ * });
2007
+ * ```
2008
+ */
2009
+ type FlowSpec<Nodes extends NodeTable, Input, FlowTags extends OutcomeTags, Table> = {
2010
+ nodes: Nodes;
2011
+ start: NodeNamesFor<NoInfer<Nodes>, NoInfer<Input>>;
2012
+ edges: Table & CheckedEdges<NoInfer<Nodes>, NoInfer<FlowTags>, NoInfer<Table>>;
2013
+ input?: TypeTag<Input>;
2014
+ outcomes?: FlowTags;
2015
+ };
2016
+ /**
2017
+ * The node context as the runner builds it. Every typed `NodeContext` is assignable to it.
2018
+ */
2019
+ type AnyNodeContext = {
2020
+ input: unknown;
2021
+ player: unknown;
2022
+ session: unknown;
2023
+ rng: RngView;
2024
+ fx: NodeFx;
2025
+ out: Readonly<Record<string, (payload: never) => Result>>;
2026
+ signal: AbortSignal;
2027
+ now: number;
2028
+ };
2029
+ /**
2030
+ * Any node as the runner holds it. `run` is a method on purpose: method parameters are compared
2031
+ * bivariantly, so every typed `NodeDefinition` fits and the runner calls the body without a cast.
2032
+ *
2033
+ * @example
2034
+ * ```ts
2035
+ * const nodes: readonly AnyNode[] = [catchUp, merge];
2036
+ * ```
2037
+ */
2038
+ type AnyNode = AnyWired & {
2039
+ readonly kind: "node";
2040
+ readonly rest: boolean;
2041
+ readonly over: boolean;
2042
+ readonly checkpoint: boolean;
2043
+ readonly barrier: boolean;
2044
+ readonly inbox: readonly string[]; /** Id of the scene the node is shown on. Absent: the node keeps the current scene. */
2045
+ readonly scene?: string;
2046
+ run?(ctx: AnyNodeContext): Result | Promise<Result>;
2047
+ };
2048
+ /**
2049
+ * One entry of a flow's `nodes`: a node, a sub-flow or a slot. The `kind` field tells them apart.
2050
+ */
2051
+ type FlowEntry = AnyNode | AnyFlow | SlotNode;
2052
+ /**
2053
+ * Any flow as the runner holds it. Every typed `FlowDefinition` fits.
2054
+ *
2055
+ * @example
2056
+ * ```ts
2057
+ * const flows: readonly AnyFlow[] = [mainFlow, boardFlow, rewardFlow];
2058
+ * ```
2059
+ */
2060
+ type AnyFlow = AnyWired & {
2061
+ readonly kind: "flow";
2062
+ readonly id: string;
2063
+ readonly nodes: Readonly<Record<string, FlowEntry>>;
2064
+ readonly start: string;
2065
+ readonly edges: Readonly<Record<string, Readonly<Record<string, Target>>>>;
2066
+ };
2067
+ /**
2068
+ * One level of the position. The path is the frames joined: `"board/awaitIntent"`.
2069
+ *
2070
+ * @example
2071
+ * ```ts
2072
+ * const frame: Frame = { flow: "board", node: "awaitIntent", input: null };
2073
+ * ```
2074
+ */
2075
+ type Frame = {
2076
+ flow: string;
2077
+ node: string;
2078
+ input: Json;
2079
+ };
2080
+ /**
2081
+ * One taken edge. `hash` covers `path + outcome + next`, so a replay against edited code fails
2082
+ * loudly in dev.
2083
+ *
2084
+ * @example
2085
+ * ```ts
2086
+ * // The play button on "home" led into the board sub-flow.
2087
+ * const entry: JournalEntry = {
2088
+ * index: 1, path: "home", outcome: "play", payload: null,
2089
+ * next: "board/awaitIntent", now: 1_790_000_000_000, hash: "fbeb1a2f"
2090
+ * };
2091
+ * ```
2092
+ */
2093
+ type JournalEntry = {
2094
+ index: number;
2095
+ path: string;
2096
+ outcome: string;
2097
+ payload: Json;
2098
+ next: string;
2099
+ now: number;
2100
+ hash: string;
2101
+ };
2102
+ /**
2103
+ * One step of a fast walk: a player answer at a rest node, or a substituted sub-flow result.
2104
+ *
2105
+ * @example
2106
+ * ```ts
2107
+ * const route: RouteStep[] = [
2108
+ * { at: "home", intent: "play" }, // the answer "play" at the rest node "home"
2109
+ * { at: "board", result: { outcome: "left" } } // the sub-flow "board" is skipped with "left"
2110
+ * ];
2111
+ * ```
2112
+ */
2113
+ type RouteStep = {
2114
+ at: string;
2115
+ intent: string;
2116
+ payload?: Json;
2117
+ } | {
2118
+ at: string;
2119
+ result: {
2120
+ outcome: string;
2121
+ payload?: Json;
2122
+ };
2123
+ };
2124
+ /**
2125
+ * A rest node plus a state that really existed there. Serialisable. `graph` is the hash of
2126
+ * `describe()`.
2127
+ *
2128
+ * @example
2129
+ * ```ts
2130
+ * const bookmark: Bookmark = {
2131
+ * path: "board/awaitIntent", input: null,
2132
+ * player: { coins: 7 }, session: { taps: 0 },
2133
+ * rng: { seed: 42, streams: {} }, graph: "fe4d257a"
2134
+ * };
2135
+ * ```
2136
+ */
2137
+ type Bookmark = {
2138
+ path: string;
2139
+ input: Json;
2140
+ player: Json;
2141
+ session: Json;
2142
+ rng: RngState;
2143
+ graph: string;
2144
+ };
2145
+ /**
2146
+ * What `onEnter` callbacks learn about the node being entered.
2147
+ *
2148
+ * @example
2149
+ * ```ts
2150
+ * // The rest node "awaitIntent" of the sub-flow "board" is being entered.
2151
+ * const node: NodeInfo = {
2152
+ * path: "board/awaitIntent", flow: "board", node: "awaitIntent",
2153
+ * rest: true, over: false, checkpoint: false, barrier: false, scene: "board"
2154
+ * };
2155
+ * ```
2156
+ */
2157
+ type NodeInfo = {
2158
+ path: string;
2159
+ flow: string;
2160
+ node: string;
2161
+ rest: boolean;
2162
+ over: boolean;
2163
+ checkpoint: boolean;
2164
+ barrier: boolean; /** Id of the scene the node is shown on: `scenes` switches on it, `assets` pins its bundle. */
2165
+ scene?: string;
2166
+ };
2167
+ /**
2168
+ * The whole graph as JSON. Edge targets are rendered as strings: `"node"`, `"exit:win"`,
2169
+ * `"map:node"`.
2170
+ *
2171
+ * @example
2172
+ * ```ts
2173
+ * const graph: FlowGraph = app.flow.describe();
2174
+ *
2175
+ * graph.main; // "main"
2176
+ * graph.flows.board?.edges.awaitIntent?.leave; // "exit:left"; a mapped target reads "map:node"
2177
+ * graph.slots.afterOrder; // [{ feature: "reward", flow: "rewardPopup", order: 10 }]
2178
+ * ```
2179
+ */
2180
+ type FlowGraph = {
2181
+ main: string;
2182
+ flows: Record<string, {
2183
+ nodes: Record<string, GraphNode>;
2184
+ start: string;
2185
+ edges: Record<string, Record<string, string>>;
2186
+ }>;
2187
+ slots: Record<string, {
2188
+ feature: string;
2189
+ flow: string;
2190
+ order: number;
2191
+ }[]>;
2192
+ };
2193
+ /**
2194
+ * One node of `describe()`: its flags, its scene, its outcome names and, when it is one, the slot
2195
+ * it opens, the sub-flow it enters and the feature that brought it. It has no `path`: a static
2196
+ * description has no runtime position, and `flow` plus `node` address it.
2197
+ *
2198
+ * @example
2199
+ * ```ts
2200
+ * // app.flow.describe().flows.main?.nodes.board: the sub-flow "board" used as a node of "main".
2201
+ * const node: GraphNode = {
2202
+ * flow: "main", node: "board",
2203
+ * rest: false, over: false, checkpoint: false, barrier: false,
2204
+ * outcomes: ["orderComplete", "left"], subFlow: "board"
2205
+ * };
2206
+ * ```
2207
+ */
2208
+ type GraphNode = Omit<NodeInfo, "path"> & {
2209
+ outcomes: string[];
2210
+ slot?: string;
2211
+ subFlow?: string;
2212
+ owner?: string;
2213
+ };
2214
+ /**
2215
+ * Inspection of the running graph.
2216
+ *
2217
+ * @example
2218
+ * ```ts
2219
+ * // The graph rests at "home" and waits for the intent "play".
2220
+ * const state: FlowState = {
2221
+ * running: true, path: "home", stack: [{ flow: "main", node: "home", input: null }],
2222
+ * pending: { gate: ["play"] }, mode: "live"
2223
+ * };
2224
+ * ```
2225
+ */
2226
+ type FlowState = {
2227
+ running: boolean;
2228
+ path: string;
2229
+ stack: readonly Frame[];
2230
+ pending: {
2231
+ fx?: string;
2232
+ gate?: readonly string[];
2233
+ };
2234
+ mode: "live" | "fast";
2235
+ };
2236
+ /**
2237
+ * Stages of entering a node: `assets` preloads at `load`, `scenes` switches at `scene`.
2238
+ *
2239
+ * @example
2240
+ * ```ts
2241
+ * const stage: Stage = "load";
2242
+ * ```
2243
+ */
2244
+ type Stage = "load" | "scene";
2245
+ /**
2246
+ * Callback of `onEnter`. Called before `node.run`, awaited.
2247
+ *
2248
+ * @example
2249
+ * ```ts
2250
+ * // The "load" stage of a game plugin: the loop waits for it before the body of the node runs.
2251
+ * const preloadNode: EnterCallback = async (node, { signal }) => {
2252
+ * if (node.flow === "board") await preloadBoard(signal); // node.path: "board/awaitIntent"
2253
+ * };
2254
+ *
2255
+ * app.flow.onEnter("load", preloadNode);
2256
+ * ```
2257
+ */
2258
+ type EnterCallback = (node: NodeInfo, ctx: {
2259
+ mode: "live" | "fast";
2260
+ signal: AbortSignal;
2261
+ }) => void | Promise<void>;
2262
+ /**
2263
+ * The seam the fast walk and `restore` steer the running loop with. It stays absent while the
2264
+ * game just runs: `walk.ts` and `restore` create it through `loopSeam` when they need it.
2265
+ */
2266
+ type LoopSeam = {
2267
+ /** Sub-flow results a walk substitutes, by path. The loop takes each one once. */substitutions: Map<string, Result>; /** Called with the path every time the loop enters a rest node. */
2268
+ rest: ((path: string) => void)[]; /** Called every time the loop opens the gate of a rest node. The walk waits on it. */
2269
+ gateOpen: (() => void)[]; /** The bookmark the loop enters at the next turn. */
2270
+ restoring: Bookmark | undefined;
2271
+ };
2272
+ /**
2273
+ * runner module state.
2274
+ */
2275
+ type RunnerState = {
2276
+ /** Flush started by a background pause. `onStop` awaits it. */flushing: Promise<void> | undefined; /** Every flow of the graph by id, collected at `run()`. */
2277
+ flows: Map<string, AnyFlow>;
2278
+ enterCallbacks: Record<Stage, EnterCallback[]>; /** The position: one frame per nesting level. */
2279
+ stack: Frame[]; /** The stack of the last rest node: the rollback target. */
2280
+ restFrame: Frame[] | undefined; /** Inside a slot: the contribution the loop just finished. The slot continues after it. */
2281
+ slotAfter: string | undefined; /** Edges since the last checkpoint. */
2282
+ journal: JournalEntry[];
2283
+ journalIndex: number; /** The promise of `run()`. `undefined`: not started. */
2284
+ running: Promise<void> | undefined; /** Aborts the active node. */
2285
+ abort: AbortController | undefined; /** Failed transitions in a row. */
2286
+ failures: number; /** How `walk` and `restore` steer the loop. Absent until one of them needs it. */
2287
+ seam?: LoopSeam; /** The last `state()` and what it was read from. Absent until the first `state()`. */
2288
+ view?: StateView;
2289
+ };
2290
+ /**
2291
+ * The last `flow.state()` and the fields it was built from. `state()` hands the same frozen
2292
+ * object out again while none of them moved, so a watcher compares identities.
2293
+ */
2294
+ type StateView = {
2295
+ running: boolean;
2296
+ depth: number;
2297
+ top: Frame | undefined;
2298
+ open: GateSpec | undefined;
2299
+ mode: "live" | "fast";
2300
+ journalIndex: number;
2301
+ state: FlowState;
2302
+ };
2303
+ /**
2304
+ * runner module API. Its methods are spread onto the plugin root: `app.flow.run()`. `run` owns
2305
+ * the one loop, `walk` and `restore` enter a position through it, `describe`, `state` and
2306
+ * `history` inspect it.
2307
+ *
2308
+ * @example
2309
+ * ```ts
2310
+ * // A live game starts the graph once and then only reads it: answers go through the gate.
2311
+ * app.flow.run().catch(showFatal);
2312
+ * app.flow.state().path; // "home", once the transit node "boot" was played out
2313
+ * ```
2314
+ */
2315
+ type RunnerApi = {
2316
+ /**
2317
+ * Validates the graph, seals the features, loads the save and runs the one loop until `onStop`
2318
+ * aborts it. A fatal error rejects: the consumer catches it.
2319
+ *
2320
+ * @returns The promise of the running graph. It resolves when the app stops.
2321
+ * @throws {Error} When `run()` was already called.
2322
+ * @example
2323
+ * ```ts
2324
+ * // The game starts the graph from onStart and does not await it: the loop never ends.
2325
+ * createApp({
2326
+ * pluginConfigs: { flow: { mainFlow, safeNode: "home" } },
2327
+ * onStart: ctx => {
2328
+ * ctx.flow.run().catch(showFatal); // a broken graph or an unreadable save ends up here
2329
+ * }
2330
+ * });
2331
+ * ```
2332
+ */
2333
+ run(): Promise<void>;
2334
+ /**
2335
+ * Registers a callback run before every node body: `assets` preloads at `load`, `scenes`
2336
+ * switches at `scene`. Every `load` callback runs before the first `scene` callback, in
2337
+ * registration order, each awaited.
2338
+ *
2339
+ * @param stage - `"load"` or `"scene"`.
2340
+ * @param callback - Called with the node and `{ mode, signal }`, awaited.
2341
+ * @returns The unregister function.
2342
+ * @example
2343
+ * ```ts
2344
+ * // A scenes plugin switches the screen when the graph enters a node.
2345
+ * const off = app.flow.onEnter("scene", node => showScene(node.path)); // node.path: "home"
2346
+ *
2347
+ * off(); // the plugin stops: the callback is not called any more
2348
+ * ```
2349
+ */
2350
+ onEnter(stage: Stage, callback: EnterCallback): () => void;
2351
+ /**
2352
+ * Walks a route in fast mode through the running loop: it answers the gate at each step's `at`
2353
+ * and substitutes the result of every sub-flow node the route skips. A `from` bookmark is
2354
+ * entered through the same check as `restore`. The mode of the caller is put back afterwards.
2355
+ *
2356
+ * @param route - The player's answers and substituted sub-flow results, in order.
2357
+ * @param options - Walk options.
2358
+ * @param options.from - Bookmark restored before the first step.
2359
+ * @returns The state the walk ended in.
2360
+ * @throws {Error} Before `run()` was called, when the bookmark is refused, and when a step's
2361
+ * `at` is never reached.
2362
+ * @example
2363
+ * ```ts
2364
+ * // A test skips the menu and stands on the board, without a screen and without waiting.
2365
+ * const state = await app.flow.walk([{ at: "home", intent: "play" }]);
2366
+ * state.path; // "board/awaitIntent"
2367
+ *
2368
+ * // Devtools jump back to a saved position and leave the board from there.
2369
+ * await app.flow.walk([{ at: "board/awaitIntent", intent: "leave" }], { from: bookmark });
2370
+ * ```
2371
+ */
2372
+ walk(route: readonly RouteStep[], options?: {
2373
+ from?: Bookmark;
2374
+ }): Promise<FlowState>;
2375
+ /**
2376
+ * Makes a bookmark of the current rest point: the rest node plus the committed state.
2377
+ *
2378
+ * @returns The bookmark, ready for JSON.
2379
+ * @throws {Error} When the graph has no position yet.
2380
+ * @example
2381
+ * ```ts
2382
+ * // A devtools button keeps the position while the board rests.
2383
+ * const bookmark = app.flow.bookmark();
2384
+ * bookmark.path; // "board/awaitIntent"
2385
+ * JSON.stringify(bookmark); // plain data: path, input, player, session, rng and the graph hash
2386
+ * ```
2387
+ */
2388
+ bookmark(): Bookmark;
2389
+ /**
2390
+ * Replaces the state with the bookmark's and enters its node. A checkpoint is always
2391
+ * accepted; any other rest node only while the graph is unchanged.
2392
+ *
2393
+ * @param bookmark - The bookmark to enter.
2394
+ * @returns A promise that resolves once the graph rests at the bookmark's node.
2395
+ * @throws {Error} When the bookmark names no rest node of this graph, when the graph changed
2396
+ * since a bookmark of a plain rest node, and before `run()`.
2397
+ * @example
2398
+ * ```ts
2399
+ * // The next session opens where the last one stopped: state and position come back together.
2400
+ * await app.flow.restore(bookmark);
2401
+ * app.flow.state().path; // "board/awaitIntent"
2402
+ * ```
2403
+ */
2404
+ restore(bookmark: Bookmark): Promise<void>;
2405
+ /**
2406
+ * Renders the whole graph as JSON, without running the game. It reads the flows as data, so
2407
+ * it works before `run()`.
2408
+ *
2409
+ * @returns Nodes, flags, outcomes, edges, slots and who contributed.
2410
+ * @example
2411
+ * ```ts
2412
+ * // A devtools panel draws the graph of the merge game.
2413
+ * const graph = app.flow.describe();
2414
+ *
2415
+ * graph.flows.main?.edges.home; // { play: "board" }
2416
+ * graph.flows.board?.nodes.merge?.outcomes; // ["done", "rejected"]
2417
+ * ```
2418
+ */
2419
+ describe(): FlowGraph;
2420
+ /**
2421
+ * Reads where the graph stands. Frozen, and the same object while the graph did not move: no
2422
+ * edge, no gate opened or closed, no mode switch.
2423
+ *
2424
+ * @returns Whether it runs, the path, the stack, what it waits for and the mode.
2425
+ * @example
2426
+ * ```ts
2427
+ * // The screen enables only the buttons the resting node takes.
2428
+ * const { path, pending } = app.flow.state();
2429
+ * // path: "home", pending: { gate: ["play"] }
2430
+ *
2431
+ * // An editor panel redraws the position only when the graph moved.
2432
+ * app.flow.state() === app.flow.state(); // true while "home" rests
2433
+ * ```
2434
+ */
2435
+ state(): FlowState;
2436
+ /**
2437
+ * Reads the edges taken since the last checkpoint.
2438
+ *
2439
+ * @returns A copy of the journal.
2440
+ * @example
2441
+ * ```ts
2442
+ * // A bug report says why the last move was refused.
2443
+ * const last = app.flow.history().at(-1);
2444
+ * // last?.path: "board/merge", last?.outcome: "rejected", last?.payload: { reason: "empty" }
2445
+ * ```
2446
+ */
2447
+ history(): readonly JournalEntry[];
2448
+ /**
2449
+ * Switches between live and fast mode. Legal before `run()` and while the graph rests.
2450
+ *
2451
+ * @param mode - `"live"` or `"fast"`.
2452
+ * @throws {Error} When a transit node is running.
2453
+ * @example
2454
+ * ```ts
2455
+ * // A headless run plays without effects: fast mode before the app starts.
2456
+ * app.flow.setMode("fast");
2457
+ * await app.start();
2458
+ * app.flow.state().mode; // "fast"
2459
+ * ```
2460
+ */
2461
+ setMode(mode: "live" | "fast"): void;
2462
+ };
2463
+ //#endregion
2464
+ //#region src/plugins/flow/features/types.d.ts
2465
+ /**
2466
+ * One animation of a feature, as `defineAnimation` returns it. Structural on purpose: `flow`
2467
+ * stores what the game brought and never imports `anim`, which reads the rest of the object.
2468
+ *
2469
+ * @example
2470
+ * ```ts
2471
+ * const coinsFly: FeatureAnimation = { id: "coinsFly" };
2472
+ * ```
2473
+ */
2474
+ type FeatureAnimation = {
2475
+ readonly id: string;
2476
+ };
2477
+ /**
2478
+ * One interface component of a feature, as `defineComponent` returns it. Structural on purpose:
2479
+ * `ui` reads the rest of the object, `flow` only carries it.
2480
+ *
2481
+ * @example
2482
+ * ```ts
2483
+ * const rewardPopup: FeatureComponent = { name: "RewardPopup" };
2484
+ * ```
2485
+ */
2486
+ type FeatureComponent = {
2487
+ readonly name: string;
2488
+ };
2489
+ /**
2490
+ * The compiled messages of one locale: message key to what `compileStrings` wrote for it. The
2491
+ * value stays opaque here because only `i18n` calls it; `flow` passes the module on untouched.
2492
+ *
2493
+ * @example
2494
+ * ```ts
2495
+ * const en: CompiledMessagesLike = { "board.title": () => [{ kind: "text", text: "Board" }] };
2496
+ * ```
2497
+ */
2498
+ type CompiledMessagesLike = {
2499
+ readonly [key: string]: unknown;
2500
+ };
2501
+ /**
2502
+ * The text styles of a feature, as `defineTextStyles` returns them. Structural on purpose: `text`
2503
+ * reads the fields of each style, `flow` only carries the map.
2504
+ *
2505
+ * @example
2506
+ * ```ts
2507
+ * const styles: FeatureTextStyles = { kind: "textStyles", map: { title: { size: 24 } } };
2508
+ * ```
2509
+ */
2510
+ type FeatureTextStyles = {
2511
+ readonly kind: "textStyles";
2512
+ readonly map: Record<string, object>;
2513
+ };
2514
+ /**
2515
+ * What a feature brings to the game. The logic keys are typed here; the keys of later milestones
2516
+ * pass through the index signature until their plugin types them. `flow` stores them untouched,
2517
+ * and `logicOnly` drops them. The V2 screen plugins read five of them through the index signature:
2518
+ * `projections`, `systems` and `components` (`world`), `scenes` (`scenes`, `assets`, and the
2519
+ * scene-id check of `flow`) and `assets` (`assets`). The V3 interface keys are typed below.
2520
+ *
2521
+ * @example
2522
+ * ```ts
2523
+ * const description: FeatureDescription = {
2524
+ * flows: [rewardFlow],
2525
+ * contribute: { afterWin: { flow: rewardFlow, order: 10 } }
2526
+ * };
2527
+ * ```
2528
+ */
2529
+ type FeatureDescription = {
2530
+ /** Nodes the feature owns. Recorded for `describe()` and hot swap. */nodes?: readonly AnyNode[]; /** Flows the feature owns. */
2531
+ flows?: readonly AnyFlow[]; /** Slot name to the sub-flow run in that slot. */
2532
+ contribute?: Record<string, {
2533
+ flow: AnyFlow;
2534
+ order: number;
2535
+ when?: (snapshot: Snapshot) => boolean;
2536
+ }>; /** Animations the feature owns, read by `anim`. */
2537
+ animations?: readonly FeatureAnimation[]; /** Interface components the feature owns, read by `ui`. `components` stays the ECS key. */
2538
+ ui?: readonly FeatureComponent[]; /** Compiled messages per locale, ready or lazy, read by `i18n`. */
2539
+ strings?: Record<string, CompiledMessagesLike | (() => Promise<unknown>)>; /** Text styles the feature owns, read by `text`. */
2540
+ textStyles?: FeatureTextStyles;
2541
+ [later: string]: unknown;
2542
+ };
2543
+ /**
2544
+ * One sub-flow contributed to a slot.
2545
+ *
2546
+ * @example
2547
+ * ```ts
2548
+ * const contribution: Contribution = { feature: "reward", flow: rewardFlow, order: 10 };
2549
+ * ```
2550
+ */
2551
+ type Contribution = {
2552
+ feature: string;
2553
+ flow: AnyFlow;
2554
+ order: number;
2555
+ when?: (snapshot: Snapshot) => boolean;
2556
+ };
2557
+ /**
2558
+ * features module state.
2559
+ */
2560
+ type FeaturesState = {
2561
+ byName: Map<string, FeatureDescription>; /** True after `run()`: `register` throws. */
2562
+ sealed: boolean;
2563
+ };
2564
+ /**
2565
+ * features module API, `app.flow.features`: the registry every feature plugin writes itself into.
2566
+ *
2567
+ * @example
2568
+ * ```ts
2569
+ * // The game composed `rewardFeature`. A plugin above asks in its onStart what the game brought.
2570
+ * app.flow.features.all().map(feature => feature.name); // ["reward"]
2571
+ * ```
2572
+ */
2573
+ type FeaturesApi = {
2574
+ /**
2575
+ * Records what a feature brings. Called from the feature plugin's `onInit`, so every feature
2576
+ * is known before the graph is validated.
2577
+ *
2578
+ * @param name - Feature name. Shares the namespace with plugin names.
2579
+ * @param description - Nodes, flows and slot contributions of the feature.
2580
+ * @throws {Error} After `run()` sealed the registry, and for a duplicate name.
2581
+ * @example
2582
+ * ```ts
2583
+ * // A hand-written feature plugin registers itself in onInit. `defineFeature` does the same.
2584
+ * const rewardPlugin = createPlugin("reward", {
2585
+ * depends: [flowPlugin],
2586
+ * onInit: ctx => ctx.require(flowPlugin).features.register("reward", { flows: [rewardFlow] })
2587
+ * });
2588
+ * ```
2589
+ */
2590
+ register(name: string, description: FeatureDescription): void;
2591
+ /**
2592
+ * Lists every registered feature in registration order. Plugins above read it in their
2593
+ * `onStart` to pick up what they own.
2594
+ *
2595
+ * @returns A fresh list of name and description.
2596
+ * @example
2597
+ * ```ts
2598
+ * // A plugin above collects the flows of every feature.
2599
+ * app.flow.features.all(); // [{ name: "reward", description: { flows: [rewardFlow] } }]
2600
+ * ```
2601
+ */
2602
+ all(): readonly {
2603
+ name: string;
2604
+ description: FeatureDescription;
2605
+ }[];
2606
+ /**
2607
+ * Lists the sub-flows contributed to one slot, lowest `order` first. Two equal orders in one
2608
+ * slot stay in registration order here and are reported by `validate`.
2609
+ *
2610
+ * @param slotName - Name of the slot node.
2611
+ * @returns The contributions of that slot, sorted by `order`.
2612
+ * @example
2613
+ * ```ts
2614
+ * // What runs when the graph enters slot("afterOrder").
2615
+ * app.flow.features.contributions("afterOrder");
2616
+ * // [{ feature: "reward", flow: rewardFlow, order: 10 }]
2617
+ * app.flow.features.contributions("afterLoss"); // []: no feature contributes to this slot
2618
+ * ```
2619
+ */
2620
+ contributions(slotName: string): readonly Contribution[];
2621
+ };
2622
+ //#endregion
2623
+ //#region src/plugins/flow/runner/define.d.ts
2624
+ /**
2625
+ * Creates a type tag: a value that carries only a payload type. Without a type argument the
2626
+ * payload is `void`. The tag never infers its type from the place it is written in.
2627
+ *
2628
+ * @returns A tag that carries the payload type and no value.
2629
+ * @example
2630
+ * ```ts
2631
+ * // The outcomes of a node: `done` carries nothing, `orderComplete` carries the reward id.
2632
+ * const outcomes = { done: type(), orderComplete: type<{ rewardId: string }>() };
2633
+ * // outcomes.done: { kind: "type" }. The payload type exists for the compiler only.
2634
+ * ```
2635
+ */
2636
+ declare function type<Payload = void>(): TypeTag<NoInfer<Payload>>;
2637
+ /**
2638
+ * Defines a flow. The edge table is inferred as written and checked entry by entry: a missing
2639
+ * edge, an unknown target and a payload that does not fit are compile errors with a sentence.
2640
+ *
2641
+ * @param id - Flow id, unique in the game.
2642
+ * @param spec - Nodes, start node, edge table, and input and outcomes when used as a node.
2643
+ * @returns The flow as plain data.
2644
+ * @example
2645
+ * ```ts
2646
+ * // The main flow of a dice game. A missing edge or an unknown target is a compile error.
2647
+ * const mainFlow = defineFlow("main", {
2648
+ * nodes: { home, roll },
2649
+ * start: "home",
2650
+ * edges: { home: { roll: "roll" }, roll: { done: "home" } }
2651
+ * });
2652
+ * ```
2653
+ */
2654
+ declare function defineFlow<Nodes extends NodeTable, Input = void, const FlowTags extends OutcomeTags = Record<never, never>, const Table = Record<never, never>>(id: string, spec: FlowSpec<Nodes, Input, FlowTags, Table>): FlowDefinition<Input, FlowTags, Nodes>;
2655
+ /**
2656
+ * Edge target: leave the sub-flow with this outcome. The payload passes through to the parent.
2657
+ *
2658
+ * @param outcome - Outcome declared by the flow.
2659
+ * @returns The exit target as plain data.
2660
+ * @example
2661
+ * ```ts
2662
+ * // Inside the sub-flow "board": the intent "leave" ends the flow with its outcome "left".
2663
+ * edges: { awaitIntent: { leave: exit("left") } } // the target: { kind: "exit", outcome: "left" }
2664
+ * // The parent flow then follows its own edge: board: { left: "home" }.
2665
+ * ```
2666
+ */
2667
+ declare function exit<const Name extends string>(outcome: Name): Exit<Name>;
2668
+ /**
2669
+ * Edge target: adapt the payload on the edge. The mapper's parameter is annotated by the author;
2670
+ * the annotation is checked against the outcome payload, the result against the target's input.
2671
+ *
2672
+ * @param target - Name of a node of the same flow.
2673
+ * @param map - Pure function from the outcome payload to the target's input.
2674
+ * @returns The mapped target as plain data.
2675
+ * @example
2676
+ * ```ts
2677
+ * // "loadCore" fails with { reason }, "retry" takes { why }: the edge adapts the payload.
2678
+ * edges: { loadCore: { failed: to("retry", (failure: { reason: string }) => ({ why: failure.reason })) } }
2679
+ * // to("retry", map): { kind: "map", target: "retry", map }
2680
+ * ```
2681
+ */
2682
+ declare function to<const TargetName extends string, Payload, Output>(target: TargetName, map: (payload: Payload) => Output): Mapped<TargetName, Payload, Output>;
2683
+ /**
2684
+ * Defines an extension point: a node whose body is "run the contributions of this slot in order".
2685
+ * Its single outcome is `done`.
2686
+ *
2687
+ * @param name - Slot name features contribute to.
2688
+ * @returns The slot node as plain data.
2689
+ * @example
2690
+ * ```ts
2691
+ * // The main flow leaves room after a finished order. Features contribute sub-flows to it.
2692
+ * nodes: { boot, home, board: boardFlow, afterOrder: slot("afterOrder") }
2693
+ * // slot("afterOrder"): { kind: "slot", name: "afterOrder", input: …, outcomes: { done: … } }
2694
+ * ```
2695
+ */
2696
+ declare function slot(name: string): SlotNode;
2697
+ declare namespace types_d_exports {
2698
+ export { Allow, Answer, AnyFlow, AnyNode, AnyNodeContext, Api, Bookmark, BundlesOf, CheckedEdges, Config, Contribution, DefineNode, Deps, Descriptor, Edges, EnterCallback, Events, Exit, FeatureDescription, FeaturePlugin, FeaturesApi, FlowCtx, FlowDefinition, FlowGraph, FlowKit, FlowSpec, FlowState, FxApi, FxHandler, GameState, GameTypes, GateApi, GraphError, GraphNode, GuideOptions, Hint, InboxApi, JournalEntry, KernelSlice, LifecycleChanged, Mapped, NodeContext, NodeDefinition, NodeFx, NodeInfo, NodeSpec, OutcomeTags, Position, Result, RouteStep, RunnerApi, SceneIdOf, SlotNode, Stage, State, Target, TextStylesOf, TypeTag, WorldEvent };
2699
+ }
2700
+ /**
2701
+ * flow plugin events.
2702
+ *
2703
+ * @example
2704
+ * ```ts
2705
+ * // A plugin that depends on flowPlugin logs every edge the player takes.
2706
+ * createPlugin("edgeLog", {
2707
+ * depends: [flowPlugin],
2708
+ * hooks: ctx => ({ "flow:edge": ({ node, outcome }) => ctx.log.info("edge", { node, outcome }) })
2709
+ * }); // the play button on "home" logs { node: "home", outcome: "play" }
2710
+ * ```
2711
+ */
2712
+ type Events = {
2713
+ /** An edge was taken and its state committed. */"flow:edge": {
2714
+ flow: string;
2715
+ node: string;
2716
+ outcome: string;
2717
+ payload: Json;
2718
+ next: string;
2719
+ patches: {
2720
+ doc: Patch[];
2721
+ session: Patch[];
2722
+ };
2723
+ index: number;
2724
+ now: number;
2725
+ }; /** The graph reached a rest node. */
2726
+ "flow:rest": {
2727
+ path: string;
2728
+ checkpoint: boolean;
2729
+ }; /** A node failed and the graph rolled back. */
2730
+ "flow:error": {
2731
+ path: string;
2732
+ error: unknown;
2733
+ rolledBackTo: string;
2734
+ retry: boolean;
2735
+ };
2736
+ };
2737
+ /**
2738
+ * flow plugin config.
2739
+ *
2740
+ * @example
2741
+ * ```ts
2742
+ * createApp({ pluginConfigs: { flow: { mainFlow, safeNode: "home" } } });
2743
+ * ```
2744
+ */
2745
+ type Config = {
2746
+ /** The top-level flow. Required before `run()`. */mainFlow: AnyFlow | undefined; /** Path of the checkpoint entered after a failed retry. `undefined`: the main flow's `start`. */
2747
+ safeNode: string | undefined; /** Retries of a failed transition before going to `safeNode`. */
2748
+ retries: number; /** How long `onStop` waits for the active node to settle after abort, in real milliseconds. */
2749
+ settleTimeoutMs: number; /** Journal entries kept between checkpoints. */
2750
+ journalLimit: number;
2751
+ };
2752
+ /**
2753
+ * flow plugin state: one branch per module.
2754
+ */
2755
+ type State = {
2756
+ features: FeaturesState;
2757
+ fx: FxState;
2758
+ gate: GateState;
2759
+ inbox: InboxState;
2760
+ runner: RunnerState;
2761
+ };
2762
+ /**
2763
+ * flow plugin API, `app.flow`: the runner on the root, the other modules grouped.
2764
+ *
2765
+ * @example
2766
+ * ```ts
2767
+ * // The game starts the graph once. After that only answers and world events move it.
2768
+ * app.flow.run().catch(showFatal);
2769
+ *
2770
+ * app.flow.gate.answer({ intent: "play" }); // the play button: true when "home" rests
2771
+ * app.flow.inbox.post({ type: "purchased" }); // the shop SDK: waits for a node that lists it
2772
+ * ```
2773
+ */
2774
+ type Api = RunnerApi & {
2775
+ gate: GateApi;
2776
+ inbox: InboxApi;
2777
+ fx: FxApi;
2778
+ features: FeaturesApi;
2779
+ };
2780
+ /**
2781
+ * The APIs flow requires from below (`time`, `model`, `clock`), resolved once in `onInit` and
2782
+ * shared by the runner, gate, inbox and fx.
2783
+ */
2784
+ type Deps = {
2785
+ time: Api$1;
2786
+ model: Api$2;
2787
+ clock: Api$4;
2788
+ };
2789
+ /**
2790
+ * What the kernel context offers before the deps are attached. `emit` is the kernel's: `index.ts`
2791
+ * writes `events` with an annotated `register` (core spec `14` row 8), so the own events reach a
2792
+ * factory passed by direct reference (`api`, `hooks`, `onInit`, `onStart`).
2793
+ */
2794
+ type KernelSlice = PluginCtx<Config, State, Events> & {
2795
+ readonly global: object;
2796
+ readonly log: Log.LogApi;
2797
+ readonly require: Require;
2798
+ };
2799
+ /**
2800
+ * Domain context shared by the modules.
2801
+ */
2802
+ type FlowCtx = KernelSlice & {
2803
+ readonly deps: Deps;
2804
+ };
2805
+ /**
2806
+ * Payload of the one event flow listens to.
2807
+ */
2808
+ type LifecycleChanged = Events$2["lifecycle:changed"];
2809
+ /**
2810
+ * The types of one game. `player` and `session` type the node context; `assets` and `bundles`
2811
+ * are the key unions the asset scanner generates (`string` while a game has none); `strings`
2812
+ * is used from V3. Every plugin binds its own helpers to them through its `…For` binder.
2813
+ *
2814
+ * @example
2815
+ * ```ts
2816
+ * type Types = { player: Player; session: Session; assets: AssetKey; bundles: BundleKey; strings: StringTable };
2817
+ * ```
2818
+ */
2819
+ type GameTypes = {
2820
+ player: Json;
2821
+ session: Json;
2822
+ assets: string;
2823
+ bundles?: string;
2824
+ scenes?: string;
2825
+ strings: Record<string, unknown>;
2826
+ textStyles?: string;
2827
+ };
2828
+ /**
2829
+ * The text style key union of a game, `string` when the game passed none.
2830
+ *
2831
+ * @example
2832
+ * ```ts
2833
+ * type Keys = TextStylesOf<{ player: {}; session: {}; assets: string; strings: {}; textStyles: "hud.digits" }>; // "hud.digits"
2834
+ * ```
2835
+ */
2836
+ type TextStylesOf<Types extends GameTypes> = Types extends {
2837
+ textStyles: infer Keys extends string;
2838
+ } ? Keys : string;
2839
+ /**
2840
+ * The bundle key union of a game, `string` when the game passed none.
2841
+ *
2842
+ * @example
2843
+ * ```ts
2844
+ * type Keys = BundlesOf<{ player: {}; session: {}; assets: string; bundles: "board"; strings: {} }>; // "board"
2845
+ * ```
2846
+ */
2847
+ type BundlesOf<Types extends GameTypes> = Types extends {
2848
+ bundles: infer Keys extends string;
2849
+ } ? Keys : string;
2850
+ /**
2851
+ * A feature is an ordinary plugin with no API of its own; `AnyPluginInstance` is the kernel's
2852
+ * widened type for plugin lists. `logicOnly` is the same plugin reduced to the V1 keys.
2853
+ *
2854
+ * @example
2855
+ * ```ts
2856
+ * createApp({ plugins: [boardFeature.logicOnly] });
2857
+ * ```
2858
+ */
2859
+ type FeaturePlugin = AnyPluginInstance & {
2860
+ readonly logicOnly: AnyPluginInstance;
2861
+ };
2862
+ /**
2863
+ * What `flowFor` binds to a game: the node helper with the game's `player` and `session`, and
2864
+ * the flow and feature helpers, which take no game types.
2865
+ *
2866
+ * @example
2867
+ * ```ts
2868
+ * const { defineNode } = flowFor<{ player: Player; session: Session }>();
2869
+ * defineNode({ rest: true, outcomes: { play: type() }, run: ({ player }) => player.coins }); // player: Player
2870
+ * ```
2871
+ */
2872
+ type FlowKit<Game extends GameState> = {
2873
+ defineNode: DefineNode<Game>;
2874
+ defineFlow: typeof defineFlow;
2875
+ defineFeature: (name: string, description: FeatureDescription) => FeaturePlugin;
2876
+ };
2877
+ /**
2878
+ * Where the graph rests, as the `game.position` source reads it: the path, the flow and node on
2879
+ * top of the stack, and the intents the open gate waits for.
2880
+ *
2881
+ * @example
2882
+ * ```ts
2883
+ * // The board rests and waits for a tap or for leaving.
2884
+ * const position: Position = {
2885
+ * path: "board/awaitIntent", flow: "board", node: "awaitIntent", waiting: ["tap", "leave"]
2886
+ * };
2887
+ * ```
2888
+ */
2889
+ type Position = {
2890
+ path: string;
2891
+ flow: string | undefined;
2892
+ node: string | undefined;
2893
+ waiting: readonly string[];
2894
+ };
2895
+ //#endregion
2896
+ export { PauseReason as $, Config$1 as A, types_d_exports$2 as B, SceneIdOf as C, Hint as D, GuideOptions as E, Config$2 as F, SaveUnreadableError as G, PlayerStateProvider as H, Events$1 as I, RngApi as J, Snapshot as K, Json as L, Time as M, types_d_exports$1 as N, Answer as O, Api$2 as P, Events$2 as Q, Root as R, RouteStep as S, Descriptor as T, ProviderCall as U, Patch as V, SaveDoc as W, Api$3 as X, RngState as Y, Config$3 as Z, DefineNode as _, GameTypes as a, State$4 as at, JournalEntry as b, TextStylesOf as c, Events$3 as ct, exit as d, State$3 as et, slot as f, Bookmark as g, FeatureDescription as h, FeaturePlugin as i, FakeClock as it, State$1 as j, Api$1 as k, types_d_exports as l, Require as lt, type as m, BundlesOf as n, Api$4 as nt, Position as o, types_d_exports$4 as ot, to as p, StoreApi as q, Config as r, Config$4 as rt, State as s, Config$5 as st, Api as t, types_d_exports$3 as tt, defineFlow as u, FlowGraph as v, TypeTag as w, NodeInfo as x, FlowState as y, State$2 as z };