@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.
@@ -1,1908 +0,0 @@
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 } });
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
- /**
18
- * Global events. Empty: every event belongs to a plugin.
19
- *
20
- * @example
21
- * ```ts
22
- * type AppEvents = Events;
23
- * ```
24
- */
25
- type Events$3 = Record<never, never>;
26
- /**
27
- * Public API type of a plugin instance, read from its phantom carrier.
28
- * Mirrors the kernel's non-exported `ExtractPluginApi`.
29
- *
30
- * @example
31
- * ```ts
32
- * type TimeApi = ApiOf<typeof timePlugin>;
33
- * ```
34
- */
35
- type ApiOf<Plugin> = Plugin extends {
36
- readonly _phantom: {
37
- readonly api: infer PluginApi;
38
- };
39
- } ? PluginApi : never;
40
- /**
41
- * Structural type of `ctx.require`, for domain factories that resolve their own dependencies.
42
- * The bound repeats the kernel's plugin shape so the kernel's generic `require` is assignable to it.
43
- *
44
- * @example
45
- * ```ts
46
- * type LifecycleCtx = PluginCtx<Config, State, Events> & { readonly require: Require };
47
- * ```
48
- */
49
- type Require = <Plugin extends {
50
- readonly name: string;
51
- readonly spec: unknown;
52
- readonly _phantom: {
53
- readonly config: unknown;
54
- readonly state: unknown;
55
- readonly api: unknown;
56
- readonly events: Record<string, unknown>;
57
- };
58
- }>(plugin: Plugin) => ApiOf<Plugin>;
59
- declare namespace types_d_exports$4 {
60
- export { Api$4 as Api, Config$4 as Config, FrameCallback, Phase, State$4 as State, Time, TimeCtx };
61
- }
62
- /**
63
- * Frame phases, in call order.
64
- *
65
- * @example
66
- * ```ts
67
- * const phase: Phase = "animate";
68
- * ```
69
- */
70
- type Phase = "input" | "animate" | "layout" | "sync" | "signals" | "render";
71
- /**
72
- * The Time resource, in scaled milliseconds.
73
- *
74
- * @example
75
- * ```ts
76
- * const time: Time = { delta: 16, elapsed: 1600, scale: 1, frame: 100 };
77
- * ```
78
- */
79
- type Time = {
80
- delta: number;
81
- elapsed: number;
82
- scale: number;
83
- frame: number;
84
- };
85
- /**
86
- * Callback run once per frame in its phase.
87
- *
88
- * @example
89
- * ```ts
90
- * const advanceTweens: FrameCallback = time => tweens.advance(time.delta);
91
- * ```
92
- */
93
- type FrameCallback = (time: Readonly<Time>) => void;
94
- /**
95
- * time plugin config.
96
- *
97
- * @example
98
- * ```ts
99
- * const config: Config = { maxFps: 60, maxDeltaMs: 50 };
100
- * ```
101
- */
102
- type Config$4 = {
103
- /**
104
- * Frame rate cap. The default is 60. 120 is for WebViews that really deliver 120 Hz frames:
105
- * Android today; WKWebView on iOS and macOS is capped at 60 by WebKit (bug 294338).
106
- */
107
- maxFps: 30 | 60 | 120;
108
- /**
109
- * Upper bound of one frame's delta in milliseconds.
110
- */
111
- maxDeltaMs: number;
112
- };
113
- /**
114
- * time plugin state.
115
- *
116
- * @example
117
- * ```ts
118
- * const state: State = createTimeState({ global, config });
119
- * ```
120
- */
121
- type State$4 = {
122
- callbacks: Record<Phase, FrameCallback[]>;
123
- /**
124
- * Scratch array of a frame: the six callback lists as they were when the frame started.
125
- */
126
- captured: (readonly FrameCallback[])[];
127
- time: Time;
128
- paused: boolean;
129
- running: boolean;
130
- /**
131
- * True while a frame runs; guards `step` re-entry.
132
- */
133
- stepping: boolean;
134
- rafId: number | undefined;
135
- lastTimestamp: number | undefined;
136
- };
137
- /**
138
- * time plugin API.
139
- *
140
- * @example
141
- * ```ts
142
- * const time: Api = ctx.require(timePlugin);
143
- * const off = time.onFrame("animate", advanceTweens);
144
- * ```
145
- */
146
- type Api$4 = {
147
- onFrame(phase: Phase, callback: FrameCallback): () => void;
148
- snapshot(): Readonly<Time>;
149
- setScale(scale: number): void;
150
- pause(): void;
151
- resume(): void;
152
- isPaused(): boolean;
153
- isRunning(): boolean;
154
- step(deltaMs: number): void;
155
- };
156
- /**
157
- * Domain context of the time plugin.
158
- *
159
- * @example
160
- * ```ts
161
- * const api = createTimeApi(ctx satisfies TimeCtx);
162
- * ```
163
- */
164
- type TimeCtx = PluginCtx<Config$4, State$4> & {
165
- readonly global: object;
166
- readonly log: Log.LogApi;
167
- };
168
- declare namespace types_d_exports$3 {
169
- export { Api$3 as Api, Config$3 as Config, Events$2 as Events, LifecycleCtx, PauseReason, State$3 as State };
170
- }
171
- /**
172
- * Why the game is paused. The named reasons are the engine's own; any other string is allowed.
173
- *
174
- * @example
175
- * ```ts
176
- * const reason: PauseReason = "background";
177
- * ```
178
- */
179
- type PauseReason = "background" | "devtools" | "system-dialog" | "device-lost" | (string & {});
180
- /**
181
- * lifecycle plugin events.
182
- *
183
- * @example
184
- * ```ts
185
- * const onChanged = (payload: Events["lifecycle:changed"]) => payload.resumed;
186
- * ```
187
- */
188
- type Events$2 = {
189
- /**
190
- * The pause stack changed. `reason` and `action` say what changed; `resumed` is true only on
191
- * the change that emptied the stack.
192
- */
193
- "lifecycle:changed": {
194
- reason: PauseReason;
195
- action: "push" | "pop";
196
- reasons: readonly PauseReason[];
197
- paused: boolean;
198
- resumed: boolean;
199
- };
200
- };
201
- /**
202
- * lifecycle plugin config: none. The plugin has no tunable behaviour.
203
- *
204
- * @example
205
- * ```ts
206
- * const config: Config = {};
207
- * ```
208
- */
209
- type Config$3 = Record<string, never>;
210
- /**
211
- * lifecycle plugin state: the pause reasons in insertion order, no duplicates.
212
- *
213
- * @example
214
- * ```ts
215
- * const state: State = { reasons: ["background"] };
216
- * ```
217
- */
218
- type State$3 = {
219
- reasons: PauseReason[];
220
- };
221
- /**
222
- * lifecycle plugin API.
223
- *
224
- * @example
225
- * ```ts
226
- * const lifecycle: Api = ctx.require(lifecyclePlugin);
227
- * lifecycle.push("background");
228
- * ```
229
- */
230
- type Api$3 = {
231
- push(reason: PauseReason): void;
232
- pop(reason: PauseReason): void;
233
- reasons(): readonly PauseReason[];
234
- isPaused(): boolean;
235
- };
236
- /**
237
- * Domain context: the kernel slice with `require`, used to reach `time`.
238
- * `emit` is a method signature on purpose: a property-typed `emit` breaks the kernel's event
239
- * inference when a factory is passed to `createPlugin` by direct reference (`api`).
240
- *
241
- * @example
242
- * ```ts
243
- * const api = createLifecycleApi(ctx satisfies LifecycleCtx);
244
- * ```
245
- */
246
- type LifecycleCtx = Omit<PluginCtx<Config$3, State$3, Events$2>, "emit"> & {
247
- emit<Name extends keyof Events$2>(name: Name, payload: Events$2[Name]): void;
248
- readonly require: Require;
249
- };
250
- //#endregion
251
- //#region src/plugins/model/rng/types.d.ts
252
- /**
253
- * @file model/rng — type definitions.
254
- */
255
- /**
256
- * Persisted randomness: one uint32 per stream id.
257
- *
258
- * @example
259
- * ```ts
260
- * const rng: RngState = { seed: 42, streams: { "chest:42": 123456789 } };
261
- * ```
262
- */
263
- type RngState = {
264
- seed: number;
265
- streams: Record<string, number>;
266
- };
267
- /**
268
- * One deterministic stream. Integers only.
269
- *
270
- * @example
271
- * ```ts
272
- * const reward = transaction.rng.stream("chest:42").pick(rewards);
273
- * ```
274
- */
275
- type RngStream = {
276
- int(maxExclusive: number): number;
277
- range(min: number, maxInclusive: number): number;
278
- pick<Item>(items: readonly Item[]): Item;
279
- weighted<Entry extends {
280
- weight: number;
281
- }>(table: readonly Entry[]): Entry;
282
- chance(numerator: number, denominator: number): boolean;
283
- };
284
- /**
285
- * View over an rng branch, draft or frozen.
286
- *
287
- * @example
288
- * ```ts
289
- * const view: RngView = createRngView(doc.rng);
290
- * view.stream("chest:42").int(6);
291
- * ```
292
- */
293
- type RngView = {
294
- stream(id: string): RngStream;
295
- };
296
- /**
297
- * rng module API.
298
- *
299
- * @example
300
- * ```ts
301
- * const state = ctx.require(modelPlugin).rng.peek("chest:42");
302
- * ```
303
- */
304
- type RngApi = {
305
- peek(id: string): number | undefined;
306
- };
307
- //#endregion
308
- //#region src/plugins/model/store/types.d.ts
309
- /**
310
- * JSON patch produced by a commit.
311
- *
312
- * @example
313
- * ```ts
314
- * const patch: Patch = { op: "replace", path: ["player", "coins"], value: 12 };
315
- * ```
316
- */
317
- type Patch = {
318
- op: "add" | "remove" | "replace";
319
- path: (string | number)[];
320
- value?: Json;
321
- };
322
- /**
323
- * The persisted document.
324
- *
325
- * @example
326
- * ```ts
327
- * const doc: SaveDoc = { player: { coins: 0 }, rng: { seed: 42, streams: {} } };
328
- * ```
329
- */
330
- type SaveDoc = {
331
- player: Json;
332
- rng: RngState;
333
- };
334
- /**
335
- * The save seam implemented by the application layer. `state` is the whole SaveDoc.
336
- *
337
- * @example
338
- * ```ts
339
- * createApp({ pluginConfigs: { model: { playerProvider: memory() } } });
340
- * ```
341
- */
342
- type PlayerStateProvider = {
343
- /** `null` means a new player. */load(): Promise<{
344
- state: Json;
345
- version: number;
346
- } | null>; /** Called at rest nodes. The provider accumulates and debounces writes. */
347
- commit(patches: Patch[], version: number): void; /** Called after a barrier node. Resolves when the data is durable. */
348
- commitDurable(patches: Patch[], txId: string, version: number): Promise<void>; /** Background, close. */
349
- flush(): Promise<void>;
350
- };
351
- /**
352
- * One recorded call of the in-memory provider.
353
- *
354
- * @example
355
- * ```ts
356
- * expect(provider.calls).toEqual([{ method: "load" }, { method: "flush" }]);
357
- * ```
358
- */
359
- type ProviderCall = {
360
- method: "load";
361
- } | {
362
- method: "commit";
363
- patches: Patch[];
364
- version: number;
365
- } | {
366
- method: "commitDurable";
367
- patches: Patch[];
368
- txId: string;
369
- version: number;
370
- } | {
371
- method: "flush";
372
- };
373
- /**
374
- * One step of the save migration chain.
375
- *
376
- * @example
377
- * ```ts
378
- * const addCoins: Migration = { from: 1, up: state => ({ player: state, coins: 0 }) };
379
- * ```
380
- */
381
- type Migration = {
382
- from: number;
383
- up(state: Json): Json;
384
- };
385
- /**
386
- * Frozen view of committed state.
387
- *
388
- * @example
389
- * ```ts
390
- * const { player, session }: Snapshot = model.store.snapshot();
391
- * ```
392
- */
393
- type Snapshot = {
394
- readonly player: Json;
395
- readonly session: Json;
396
- readonly rng: Readonly<RngState>;
397
- };
398
- /**
399
- * Result of a commit.
400
- *
401
- * @example
402
- * ```ts
403
- * const { patches, roots }: CommitResult = transaction.commit();
404
- * ```
405
- */
406
- type CommitResult = {
407
- patches: {
408
- doc: Patch[];
409
- session: Patch[];
410
- };
411
- roots: Root[];
412
- };
413
- /**
414
- * Open transaction handed to one node run.
415
- *
416
- * @example
417
- * ```ts
418
- * const transaction: Transaction = model.store.begin();
419
- * transaction.commit();
420
- * ```
421
- */
422
- type Transaction = {
423
- /** Mutable draft of the player tree. */player: Json; /** Mutable draft of the session tree. */
424
- session: Json; /** Rng view bound to the draft `doc.rng`. */
425
- rng: RngView;
426
- commit(): CommitResult;
427
- discard(): void;
428
- };
429
- /**
430
- * store module state.
431
- *
432
- * @example
433
- * ```ts
434
- * const store: StoreState = createStoreState(config);
435
- * ```
436
- */
437
- type StoreState = {
438
- /** Frozen save document. */doc: SaveDoc; /** Frozen session tree. */
439
- session: Json; /** Frozen trees of the last rest point. */
440
- restPoint: {
441
- doc: SaveDoc;
442
- session: Json;
443
- } | undefined; /** Doc patches since the last provider commit. */
444
- pending: Patch[];
445
- transaction: Transaction | undefined;
446
- loaded: boolean;
447
- provider: PlayerStateProvider;
448
- };
449
- /**
450
- * store module API.
451
- *
452
- * @example
453
- * ```ts
454
- * const store: StoreApi = ctx.require(modelPlugin).store;
455
- * await store.load();
456
- * ```
457
- */
458
- type StoreApi = {
459
- load(): Promise<void>;
460
- snapshot(): Snapshot;
461
- begin(): Transaction;
462
- markRest(): void;
463
- markBarrier(txId: string): Promise<void>;
464
- rollback(): void;
465
- restore(input: {
466
- player: Json;
467
- session?: Json;
468
- rng?: RngState;
469
- }): void;
470
- flush(): Promise<void>;
471
- };
472
- /**
473
- * Thrown by `load()` when the save cannot be read by this build.
474
- *
475
- * @example
476
- * ```ts
477
- * if (error instanceof SaveUnreadableError) showSaveScreen(error);
478
- * ```
479
- */
480
- declare class SaveUnreadableError extends Error {
481
- readonly savedVersion: number;
482
- readonly schemaVersion: number;
483
- /**
484
- * Creates the error.
485
- *
486
- * @param savedVersion - Version found in the save.
487
- * @param schemaVersion - Version this build writes.
488
- * @param cause - The underlying failure.
489
- * @example
490
- * ```ts
491
- * throw new SaveUnreadableError(3, 2, undefined);
492
- * ```
493
- */
494
- constructor(savedVersion: number, schemaVersion: number, cause: unknown);
495
- }
496
- declare namespace types_d_exports$2 {
497
- 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 };
498
- }
499
- /**
500
- * Plain JSON value. Everything in state is JSON.
501
- *
502
- * @example
503
- * ```ts
504
- * const player: Json = { coins: 10, inventory: ["key"] };
505
- * ```
506
- */
507
- type Json = null | boolean | number | string | Json[] | {
508
- [key: string]: Json;
509
- };
510
- /**
511
- * State roots reported by `model:committed`.
512
- *
513
- * @example
514
- * ```ts
515
- * const roots: Root[] = ["player", "rng"];
516
- * ```
517
- */
518
- type Root = "player" | "session" | "rng";
519
- /**
520
- * model plugin events.
521
- *
522
- * @example
523
- * ```ts
524
- * hooks: { "model:committed": ({ roots, cause }) => reconcile(roots, cause) }
525
- * ```
526
- */
527
- type Events$1 = {
528
- /** Committed state changed. Projections reconcile from the snapshot. */"model:committed": {
529
- roots: readonly Root[];
530
- cause: "edge" | "rollback" | "restore" | "load";
531
- };
532
- };
533
- /**
534
- * model plugin config.
535
- *
536
- * @example
537
- * ```ts
538
- * createApp({ pluginConfigs: { model: { initialPlayer: { coins: 0 }, seed: 42 } } });
539
- * ```
540
- */
541
- type Config$2 = {
542
- /** The save seam. `undefined`: an in-memory provider, nothing is persisted. */playerProvider: PlayerStateProvider | undefined; /** Player state of a new player. Deep-cloned. */
543
- initialPlayer: Json; /** Session state at every start. Deep-cloned. */
544
- initialSession: Json; /** "from-save": a new player gets a random seed once; a number fixes it (tests). */
545
- seed: "from-save" | number; /** Version of the save schema written by this build. */
546
- schemaVersion: number; /** Ordered chain; `up` of `from: n` produces version `n + 1`. */
547
- migrations: readonly Migration[];
548
- };
549
- /**
550
- * model plugin state: one branch per module.
551
- *
552
- * @example
553
- * ```ts
554
- * const state: State = createModelState({ global, config });
555
- * ```
556
- */
557
- type State$2 = {
558
- store: StoreState;
559
- rng: Record<string, never>;
560
- };
561
- /**
562
- * model plugin API, grouped by module.
563
- *
564
- * @example
565
- * ```ts
566
- * const model: Api = ctx.require(modelPlugin);
567
- * model.store.snapshot();
568
- * ```
569
- */
570
- type Api$2 = {
571
- store: StoreApi;
572
- rng: RngApi;
573
- };
574
- /**
575
- * Domain context shared by the modules.
576
- * `emit` is a method signature on purpose: a property-typed `emit` breaks the kernel's event
577
- * inference when a factory is passed to `createPlugin` by direct reference (`onStart`).
578
- *
579
- * @example
580
- * ```ts
581
- * export function createStoreApi(ctx: ModelCtx, deps: { createRngView: CreateRngView }): StoreApi;
582
- * ```
583
- */
584
- type ModelCtx = Omit<PluginCtx<Config$2, State$2, Events$1>, "emit"> & {
585
- emit<Name extends keyof Events$1>(name: Name, payload: Events$1[Name]): void;
586
- readonly global: object;
587
- readonly log: Log.LogApi;
588
- };
589
- declare namespace types_d_exports$1 {
590
- export { Api$1 as Api, ClockCtx, ClockSource, Config$1 as Config, Elapsed, FakeClock, State$1 as State };
591
- }
592
- /**
593
- * Source of time and timers. The system source is the only code that reads the device clock.
594
- *
595
- * @example
596
- * ```ts
597
- * const source: ClockSource = fakeClock(1000);
598
- * ```
599
- */
600
- type ClockSource = {
601
- now(): number;
602
- setTimer(callback: () => void, delayMs: number): unknown;
603
- clearTimer(handle: unknown): void;
604
- };
605
- /**
606
- * Fake source for tests.
607
- *
608
- * @example
609
- * ```ts
610
- * const clock: FakeClock = fakeClock();
611
- * clock.advance(5000);
612
- * ```
613
- */
614
- type FakeClock = ClockSource & {
615
- advance(ms: number): void;
616
- set(moment: number): void;
617
- };
618
- /**
619
- * Input delivered when a due moment arrives or the game resumes.
620
- *
621
- * @example
622
- * ```ts
623
- * const input: Elapsed = { now: clock.now() };
624
- * ```
625
- */
626
- type Elapsed = {
627
- now: number;
628
- };
629
- /**
630
- * clock plugin config.
631
- *
632
- * @example
633
- * ```ts
634
- * const config: Config = { source: undefined };
635
- * ```
636
- */
637
- type Config$1 = {
638
- /**
639
- * Time source. `undefined` means the system source.
640
- */
641
- source: ClockSource | undefined;
642
- };
643
- /**
644
- * clock plugin state.
645
- *
646
- * @example
647
- * ```ts
648
- * const state: State = createClockState({ global, config });
649
- * ```
650
- */
651
- type State$1 = {
652
- source: ClockSource;
653
- last: number;
654
- dueAt: number | undefined;
655
- handle: unknown;
656
- listeners: Array<(input: Elapsed) => void>;
657
- };
658
- /**
659
- * clock plugin API.
660
- *
661
- * @example
662
- * ```ts
663
- * const clock: Api = ctx.require(clockPlugin);
664
- * clock.scheduleAt(clock.now() + 60_000);
665
- * ```
666
- */
667
- type Api$1 = {
668
- now(): number;
669
- scheduleAt(moment: number | undefined): void;
670
- onElapsed(listener: (input: Elapsed) => void): () => void;
671
- poke(): void;
672
- dueAt(): number | undefined;
673
- };
674
- /**
675
- * Domain context of the clock plugin.
676
- *
677
- * @example
678
- * ```ts
679
- * const api = createClockApi(ctx satisfies ClockCtx);
680
- * ```
681
- */
682
- type ClockCtx = PluginCtx<Config$1, State$1> & {
683
- readonly global: object;
684
- };
685
- //#endregion
686
- //#region src/plugins/flow/gate/types.d.ts
687
- /**
688
- * One answer of the player, of an agent or of a test.
689
- *
690
- * @example
691
- * ```ts
692
- * const answer: Answer = { intent: "merge", payload: { from: "c2", to: "c3" } };
693
- * ```
694
- */
695
- type Answer = {
696
- intent: string;
697
- payload?: Json;
698
- };
699
- /**
700
- * The one answer a `guide` lets through. `payload` is compared by deep equality when present.
701
- *
702
- * @example
703
- * ```ts
704
- * const allow: Allow = { intent: "merge", payload: { from: "c2", to: "c3" } };
705
- * ```
706
- */
707
- type Allow = {
708
- intent: string;
709
- payload?: Json;
710
- };
711
- /**
712
- * What an open gate accepts.
713
- *
714
- * @example
715
- * ```ts
716
- * const spec: GateSpec = { allowed: ["play", "shop"] };
717
- * ```
718
- */
719
- type GateSpec = {
720
- allowed: readonly string[];
721
- };
722
- /**
723
- * gate module state.
724
- *
725
- * @example
726
- * ```ts
727
- * const gate: GateState = createGateState();
728
- * ```
729
- */
730
- type GateState = {
731
- /** The spec of the open gate. `undefined`: the gate is closed. */open: GateSpec | undefined; /** Resolver of the promise returned by `open`. */
732
- resolve: ((answer: Answer) => void) | undefined; /** An answer given while the gate was closed, kept for one frame. */
733
- held: {
734
- answer: Answer;
735
- frame: number;
736
- } | undefined; /** The one answer a running `guide` lets through. */
737
- narrow: Allow | undefined; /** True while a pointer is down. Entering an `over` node waits for false. */
738
- pointerActive: boolean; /** Ends the pending pointer wait of the runner. Set while the wait runs, so a stop needs no frame. */
739
- wake: (() => void) | undefined;
740
- };
741
- /**
742
- * gate module API: the single entry of player answers.
743
- *
744
- * @example
745
- * ```ts
746
- * const accepted = app.flow.gate.answer({ intent: "play" });
747
- * ```
748
- */
749
- type GateApi = {
750
- answer(answer: Answer): boolean;
751
- pointer(active: boolean): void;
752
- state(): {
753
- open: boolean;
754
- allowed: readonly string[];
755
- narrowed: boolean;
756
- };
757
- };
758
- //#endregion
759
- //#region src/plugins/flow/fx/types.d.ts
760
- /**
761
- * An effect a node awaits. With `answers` the runner opens the gate for those intents.
762
- * `cosmetic: true` swallows a handler error and resolves `undefined`.
763
- *
764
- * @example
765
- * ```ts
766
- * const descriptor: Descriptor = { kind: "popup", payload: { name: "Retry" }, answers: ["again"] };
767
- * ```
768
- */
769
- type Descriptor = {
770
- kind: string;
771
- payload?: Json;
772
- answers?: readonly string[];
773
- cosmetic?: boolean;
774
- };
775
- /**
776
- * A fire-and-forget cosmetic effect. Released only after the commit of its transaction.
777
- *
778
- * @example
779
- * ```ts
780
- * const sparkle: Hint = { kind: "sparkle", payload: { cell: "c3" }, hint: true };
781
- * ```
782
- */
783
- type Hint = {
784
- kind: string;
785
- payload?: Json;
786
- hint: true;
787
- };
788
- /**
789
- * Options of the `guide` descriptor: the one allowed answer and its visual part.
790
- *
791
- * @example
792
- * ```ts
793
- * const options: GuideOptions = { allow: { intent: "merge" }, hand: "drag", text: "Merge them" };
794
- * ```
795
- */
796
- type GuideOptions = {
797
- allow: Allow;
798
- highlight?: readonly string[];
799
- hand?: "tap" | "drag";
800
- text?: string;
801
- };
802
- /**
803
- * Handler of one effect kind, registered by a plugin above `flow`.
804
- *
805
- * @example
806
- * ```ts
807
- * const playSound: FxHandler = (descriptor, { signal }) => audio.play(descriptor.payload, signal);
808
- * ```
809
- */
810
- type FxHandler = (descriptor: Descriptor | Hint, ctx: {
811
- signal: AbortSignal;
812
- mode: "live" | "fast";
813
- }) => unknown | Promise<unknown>;
814
- /**
815
- * fx module state.
816
- *
817
- * @example
818
- * ```ts
819
- * const fx: FxState = createFxState();
820
- * ```
821
- */
822
- type FxState = {
823
- /** One handler per effect kind. */handlers: Map<string, {
824
- run: FxHandler;
825
- runInFast: boolean;
826
- }>; /** Hints of the open transaction. */
827
- buffered: Hint[]; /** Completions waiting for the `signals` phase, in start order. */
828
- settled: Array<() => void>;
829
- mode: "live" | "fast";
830
- };
831
- /**
832
- * fx module API: the effects gateway.
833
- *
834
- * @example
835
- * ```ts
836
- * const off = app.flow.fx.handle("sfx", playSound);
837
- * ```
838
- */
839
- type FxApi = {
840
- handle(kind: string, handler: FxHandler, options?: {
841
- runInFast?: boolean;
842
- }): () => void;
843
- dispatch(descriptor: Descriptor | Hint): void;
844
- };
845
- /**
846
- * The `fx` of a node context: awaited effects by call, cosmetic hints by `emit`.
847
- *
848
- * @example
849
- * ```ts
850
- * const pick = await fx(popup("Retry", ["again", "home"]));
851
- * fx.emit(hint("sparkle", { cell: "c3" }));
852
- * ```
853
- */
854
- type NodeFx = ((descriptor: Descriptor) => Promise<unknown>) & {
855
- emit(hint: Hint): void;
856
- };
857
- //#endregion
858
- //#region src/plugins/flow/inbox/types.d.ts
859
- /**
860
- * One event of the world: time that passed, a push message, a purchase that arrived.
861
- *
862
- * @example
863
- * ```ts
864
- * const event: WorldEvent = { type: "elapsed", payload: { now: 1000 } };
865
- * ```
866
- */
867
- type WorldEvent = {
868
- type: string;
869
- payload?: Json;
870
- };
871
- /**
872
- * inbox module state.
873
- *
874
- * @example
875
- * ```ts
876
- * const inbox: InboxState = createInboxState();
877
- * ```
878
- */
879
- type InboxState = {
880
- /** 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. */
881
- listeners: Array<() => void>;
882
- };
883
- /**
884
- * inbox module API.
885
- *
886
- * @example
887
- * ```ts
888
- * app.flow.inbox.post({ type: "elapsed", payload: { now: 1000 } });
889
- * ```
890
- */
891
- type InboxApi = {
892
- post(event: WorldEvent): void;
893
- };
894
- //#endregion
895
- //#region src/plugins/flow/runner/types.d.ts
896
- /**
897
- * A value that carries only a type. Invariant in `Payload`, so `{ stars }` never passes for
898
- * `{ stars, moves }` in either direction.
899
- *
900
- * @example
901
- * ```ts
902
- * const stars: TypeTag<{ stars: number }> = type<{ stars: number }>();
903
- * ```
904
- */
905
- type TypeTag<Payload> = {
906
- readonly kind: "type";
907
- readonly $type?: (value: Payload) => Payload;
908
- };
909
- /**
910
- * Any type tag. The bound of tag records: it names no payload, so `type()` infers nothing from it.
911
- *
912
- * @example
913
- * ```ts
914
- * const tag: AnyTypeTag = type<{ level: number }>();
915
- * ```
916
- */
917
- type AnyTypeTag = {
918
- readonly kind: "type";
919
- };
920
- /**
921
- * Outcome name to the type tag of its payload.
922
- *
923
- * @example
924
- * ```ts
925
- * const outcomes = { done: type(), failed: type<{ reason: string }>() } satisfies OutcomeTags;
926
- * ```
927
- */
928
- type OutcomeTags = Readonly<Record<string, AnyTypeTag>>;
929
- /**
930
- * The payload type carried by a type tag.
931
- *
932
- * @example
933
- * ```ts
934
- * type Reason = PayloadOf<TypeTag<{ reason: string }>>; // { reason: string }
935
- * ```
936
- */
937
- type PayloadOf<Tag> = Tag extends TypeTag<infer Payload> ? Payload : never;
938
- /**
939
- * The part of the game types a node sees. Inside the engine both trees are `Json`;
940
- * `defineGame<Types>()` narrows them for the game at the type level only.
941
- *
942
- * @example
943
- * ```ts
944
- * const game: GameState = { player: { lives: 3 }, session: { visits: 0 } };
945
- * ```
946
- */
947
- type GameState = {
948
- player: Json;
949
- session: Json;
950
- };
951
- /**
952
- * Whether a payload or input type is `void`: an outcome without data, a node without input.
953
- *
954
- * @example
955
- * ```ts
956
- * type Empty = IsVoid<PayloadOf<TypeTag<void>>>; // true
957
- * ```
958
- */
959
- type IsVoid<Payload> = [Payload] extends [void] ? true : false;
960
- /**
961
- * What `out.name(data)` returns. `run` must return one of these; a bare string is not accepted.
962
- * `Name` is the union of the node's outcome names; without a type argument it is the widened
963
- * result the runner reads.
964
- *
965
- * @example
966
- * ```ts
967
- * const result: Result<"done" | "rejected"> = out.done();
968
- * ```
969
- */
970
- type Result<Name extends string = string> = {
971
- readonly outcome: Name;
972
- readonly payload: Json;
973
- };
974
- /**
975
- * The `out` of a node context: one method per declared outcome, without an argument when the
976
- * payload is `void`.
977
- *
978
- * @example
979
- * ```ts
980
- * run: ({ out }) => (broken ? out.failed({ reason: "clock" }) : out.done())
981
- * ```
982
- */
983
- 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> };
984
- /**
985
- * The one object a node body receives. `player` and `session` are drafts of the open
986
- * transaction, `rng` is its view, `now` is `clock.now()` read once at node entry.
987
- *
988
- * @example
989
- * ```ts
990
- * run: ({ input, player, out }: NodeContext<Game, { level: number }, Tags>) => out.done()
991
- * ```
992
- */
993
- type NodeContext<Game extends GameState = GameState, Input = void, Tags extends OutcomeTags = OutcomeTags> = {
994
- input: Input;
995
- player: Game["player"];
996
- session: Game["session"];
997
- rng: RngView;
998
- fx: NodeFx;
999
- out: Out<Tags>;
1000
- signal: AbortSignal;
1001
- now: number;
1002
- };
1003
- /**
1004
- * The body of a node: plain `await` code that ends with `out.name(data)`.
1005
- *
1006
- * @example
1007
- * ```ts
1008
- * const run: NodeRun<Game, void, { done: TypeTag<void> }> = ({ out }) => out.done();
1009
- * ```
1010
- */
1011
- type NodeRun<Game extends GameState, Input, Tags extends OutcomeTags> = (ctx: NodeContext<Game, Input, Tags>) => Result<keyof Tags & string> | Promise<Result<keyof Tags & string>>;
1012
- /**
1013
- * The part of a node, or of a flow used as a node, that an edge table looks at.
1014
- *
1015
- * @example
1016
- * ```ts
1017
- * const wired: Wired<{ level: number }, { win: TypeTag<void> }> = levelFlow;
1018
- * ```
1019
- */
1020
- type Wired<Input, Tags extends OutcomeTags> = {
1021
- readonly input: TypeTag<Input>;
1022
- readonly outcomes: Tags;
1023
- };
1024
- /**
1025
- * Anything that can sit in the `nodes` of a flow: a node, a sub-flow or a slot.
1026
- *
1027
- * @example
1028
- * ```ts
1029
- * const entry: AnyWired = catchUp;
1030
- * ```
1031
- */
1032
- type AnyWired = {
1033
- readonly input: AnyTypeTag;
1034
- readonly outcomes: OutcomeTags;
1035
- };
1036
- /**
1037
- * Node name to node: the `nodes` of one flow.
1038
- *
1039
- * @example
1040
- * ```ts
1041
- * const nodes = { catchUp, awaitIntent, merge } satisfies NodeTable;
1042
- * ```
1043
- */
1044
- type NodeTable = Readonly<Record<string, AnyWired>>;
1045
- /**
1046
- * A node as `defineNode` returns it: plain data plus the optional body.
1047
- *
1048
- * @example
1049
- * ```ts
1050
- * const merge: NodeDefinition<{ from: string; to: string }, { done: TypeTag<void> }> = defineNode({
1051
- * input: type<{ from: string; to: string }>(),
1052
- * outcomes: { done: type() },
1053
- * run: ({ out }) => out.done()
1054
- * });
1055
- * ```
1056
- */
1057
- type NodeDefinition<Input, Tags extends OutcomeTags, Game extends GameState = GameState> = Wired<Input, Tags> & {
1058
- readonly kind: "node";
1059
- readonly rest: boolean;
1060
- readonly over: boolean;
1061
- readonly checkpoint: boolean;
1062
- readonly barrier: boolean;
1063
- readonly inbox: readonly (keyof Tags & string)[];
1064
- readonly run?: NodeRun<Game, Input, Tags>;
1065
- };
1066
- /**
1067
- * What the author passes to `defineNode`. `run` is required unless `rest: true`: a rest node
1068
- * without a body is a pure wait.
1069
- *
1070
- * @example
1071
- * ```ts
1072
- * const spec: NodeSpec<Game, void, { play: TypeTag<void> }> = {
1073
- * rest: true,
1074
- * checkpoint: true,
1075
- * outcomes: { play: type() }
1076
- * };
1077
- * ```
1078
- */
1079
- type NodeSpec<Game extends GameState, Input, Tags extends OutcomeTags> = {
1080
- input?: TypeTag<Input>;
1081
- outcomes: Tags;
1082
- over?: boolean;
1083
- checkpoint?: boolean;
1084
- barrier?: boolean;
1085
- inbox?: readonly (keyof Tags & string)[];
1086
- } & ({
1087
- rest: true;
1088
- run?: NodeRun<Game, Input, Tags>;
1089
- } | {
1090
- rest?: false;
1091
- run: NodeRun<Game, Input, Tags>;
1092
- });
1093
- /**
1094
- * The signature of `defineNode` bound to one game. `defineGame<Types>()` returns it.
1095
- *
1096
- * @example
1097
- * ```ts
1098
- * const defineGameNode: DefineNode<{ player: Player; session: Session }> = defineNode;
1099
- * ```
1100
- */
1101
- type DefineNode<Game extends GameState> = <Input = void, const Tags extends OutcomeTags = OutcomeTags>(spec: NodeSpec<Game, Input, Tags>) => NodeDefinition<Input, Tags, Game>;
1102
- /**
1103
- * An extension point inside a flow: a node whose body is "run the contributions in order".
1104
- * Its single outcome is `done`.
1105
- *
1106
- * @example
1107
- * ```ts
1108
- * const afterWin: SlotNode = slot("afterWin");
1109
- * ```
1110
- */
1111
- type SlotNode = Wired<void, {
1112
- readonly done: TypeTag<void>;
1113
- }> & {
1114
- readonly kind: "slot";
1115
- readonly name: string;
1116
- };
1117
- /**
1118
- * Edge target that leaves the sub-flow with this outcome. The payload passes through.
1119
- *
1120
- * @example
1121
- * ```ts
1122
- * const leave: Exit<"win"> = exit("win");
1123
- * ```
1124
- */
1125
- type Exit<Name extends string = string> = {
1126
- readonly kind: "exit";
1127
- readonly outcome: Name;
1128
- };
1129
- /**
1130
- * Edge target that adapts the payload on the edge. `map` is a property on purpose: the mapper's
1131
- * parameter annotation is checked strictly against the outcome payload.
1132
- *
1133
- * @example
1134
- * ```ts
1135
- * const adapt: Mapped<"retry", { reason: string }, { why: string }> = to(
1136
- * "retry",
1137
- * (failure: { reason: string }) => ({ why: failure.reason })
1138
- * );
1139
- * ```
1140
- */
1141
- type Mapped<TargetName extends string = string, Payload = never, Output = unknown> = {
1142
- readonly kind: "map";
1143
- readonly target: TargetName;
1144
- readonly map: (payload: Payload) => Output;
1145
- };
1146
- /**
1147
- * A mapped target as the runner calls it. `map` is a method on purpose: method parameters are
1148
- * compared bivariantly, so every checked `Mapped` fits and the runner calls it without a cast.
1149
- * The result is `unknown`: a mapper into a node without input returns nothing, and the runner
1150
- * checks that a mapped payload is plain JSON before it becomes the next input.
1151
- *
1152
- * @example
1153
- * ```ts
1154
- * const input = mapped.map(result.payload);
1155
- * ```
1156
- */
1157
- type AnyMapped = {
1158
- readonly kind: "map";
1159
- readonly target: string;
1160
- map(payload: unknown): unknown;
1161
- };
1162
- /**
1163
- * Any edge target as the runner reads it: a node name, an exit or a mapped target.
1164
- *
1165
- * @example
1166
- * ```ts
1167
- * const target: Target | undefined = flow.edges[node]?.[result.outcome];
1168
- * ```
1169
- */
1170
- type Target = string | Exit | AnyMapped;
1171
- /**
1172
- * Whether a receiver with input `Input` takes `Payload`. A receiver without input ignores the
1173
- * payload, so it accepts anything; a void outcome into a node with input is an error.
1174
- *
1175
- * @example
1176
- * ```ts
1177
- * type Fits = Accepts<{ stars: number }, void>; // true
1178
- * ```
1179
- */
1180
- type Accepts<Payload, Input> = IsVoid<Input> extends true ? true : [Payload] extends [Input] ? true : false;
1181
- /**
1182
- * The input type of a node or sub-flow.
1183
- *
1184
- * @example
1185
- * ```ts
1186
- * type MergeInput = InputOf<typeof merge>; // { from: string; to: string }
1187
- * ```
1188
- */
1189
- type InputOf<Node extends AnyWired> = PayloadOf<Node["input"]>;
1190
- /**
1191
- * Names of the nodes whose input accepts `Payload`.
1192
- *
1193
- * @example
1194
- * ```ts
1195
- * type Starts = NodeNamesFor<{ boot: typeof boot; merge: typeof merge }, void>; // "boot"
1196
- * ```
1197
- */
1198
- type NodeNamesFor<Nodes extends NodeTable, Payload> = { [Name in keyof Nodes & string]: Accepts<Payload, InputOf<Nodes[Name]>> extends true ? Name : never }[keyof Nodes & string];
1199
- /**
1200
- * Names of the flow outcomes whose payload accepts `Payload`.
1201
- *
1202
- * @example
1203
- * ```ts
1204
- * type Exits = ExitNamesFor<{ win: TypeTag<{ stars: number }> }, { stars: number }>; // "win"
1205
- * ```
1206
- */
1207
- type ExitNamesFor<FlowTags extends OutcomeTags, Payload> = { [Name in keyof FlowTags & string]: Accepts<Payload, PayloadOf<FlowTags[Name]>> extends true ? Name : never }[keyof FlowTags & string];
1208
- /**
1209
- * Every correctly typed `to(node, map)` for `Payload`.
1210
- *
1211
- * @example
1212
- * ```ts
1213
- * type Adapters = MappedFor<{ retry: typeof retry }, { reason: string }>;
1214
- * ```
1215
- */
1216
- type MappedFor<Nodes extends NodeTable, Payload> = { [Name in keyof Nodes & string]: Mapped<Name, Payload, InputOf<Nodes[Name]>> }[keyof Nodes & string];
1217
- /**
1218
- * Descriptive type: every legal target of an outcome that carries `Payload`.
1219
- *
1220
- * @example
1221
- * ```ts
1222
- * type AfterMerge = TargetFor<BoardNodes, BoardOutcomes, void>;
1223
- * ```
1224
- */
1225
- type TargetFor<Nodes extends NodeTable, FlowTags extends OutcomeTags, Payload> = NodeNamesFor<Nodes, Payload> | Exit<ExitNamesFor<FlowTags, Payload>> | MappedFor<Nodes, Payload>;
1226
- /**
1227
- * Descriptive type of an edge table: one entry per outcome of every node. Stored on
1228
- * `FlowDefinition` and read by tools; the check with readable errors is `CheckedEdges`.
1229
- *
1230
- * @example
1231
- * ```ts
1232
- * const edges: Edges<{ boot: typeof boot; home: typeof home }, Record<never, never>> = {
1233
- * boot: { done: "home" },
1234
- * home: { play: "boot" }
1235
- * };
1236
- * ```
1237
- */
1238
- 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]>> } };
1239
- /**
1240
- * A branded sentence. The compiler prints it in the "is not assignable to" position, so a wrong
1241
- * edge reads as a message with node and outcome names.
1242
- *
1243
- * @example
1244
- * ```ts
1245
- * type Problem = GraphError<'No node "awaitIntnet" in this flow (edge of outcome "done" of node "merge").'>;
1246
- * ```
1247
- */
1248
- type GraphError<Message extends string> = {
1249
- readonly $graphError: Message;
1250
- };
1251
- /**
1252
- * The loosest legal target of a flow: what a missing entry is expected to be.
1253
- *
1254
- * @example
1255
- * ```ts
1256
- * type Missing = AnyTargetOf<BoardNodes, BoardOutcomes>;
1257
- * ```
1258
- */
1259
- type AnyTargetOf<Nodes extends NodeTable, FlowTags extends OutcomeTags> = (keyof Nodes & string) | Exit<keyof FlowTags & string> | Mapped<keyof Nodes & string, never, unknown>;
1260
- /**
1261
- * Checks one entry `Value` of the inferred edge table. A legal entry stays itself; a wrong one
1262
- * becomes a `GraphError` sentence.
1263
- *
1264
- * @example
1265
- * ```ts
1266
- * type Checked = CheckTarget<BoardNodes, BoardOutcomes, "merge", "done", void, "awaitIntent">;
1267
- * ```
1268
- */
1269
- 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>;
1270
- /**
1271
- * Checking type: validates the inferred edge table `Table` entry by entry. It reports a missing
1272
- * edge, an unknown target, a payload that does not fit, an edge of an unknown outcome and an
1273
- * edge table row of an unknown node.
1274
- *
1275
- * @example
1276
- * ```ts
1277
- * type Spec = { edges: Table & CheckedEdges<Nodes, FlowTags, Table> };
1278
- * ```
1279
- */
1280
- 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.`> };
1281
- /**
1282
- * A flow as `defineFlow` returns it: plain data. A flow that declares `input` and `outcomes`
1283
- * is used as a node of another flow.
1284
- *
1285
- * @example
1286
- * ```ts
1287
- * const levelFlow: FlowDefinition<{ level: number }, { win: TypeTag<void> }, LevelNodes> =
1288
- * defineFlow("level", { input, outcomes, nodes, start: "prepare", edges });
1289
- * ```
1290
- */
1291
- type FlowDefinition<Input, FlowTags extends OutcomeTags, Nodes extends NodeTable = NodeTable> = Wired<Input, FlowTags> & {
1292
- readonly kind: "flow";
1293
- readonly id: string;
1294
- readonly nodes: Nodes;
1295
- readonly start: keyof Nodes & string;
1296
- readonly edges: Edges<Nodes, FlowTags>;
1297
- };
1298
- /**
1299
- * What the author passes to `defineFlow`. `start` must name a node whose input accepts the flow
1300
- * input; `edges` is inferred as written and validated by `CheckedEdges`.
1301
- *
1302
- * @example
1303
- * ```ts
1304
- * defineFlow("main", { nodes: { boot, home }, start: "boot", edges: { boot: { done: "home" } } });
1305
- * ```
1306
- */
1307
- type FlowSpec<Nodes extends NodeTable, Input, FlowTags extends OutcomeTags, Table> = {
1308
- nodes: Nodes;
1309
- start: NodeNamesFor<NoInfer<Nodes>, NoInfer<Input>>;
1310
- edges: Table & CheckedEdges<NoInfer<Nodes>, NoInfer<FlowTags>, NoInfer<Table>>;
1311
- input?: TypeTag<Input>;
1312
- outcomes?: FlowTags;
1313
- };
1314
- /**
1315
- * The node context as the runner builds it. Every typed `NodeContext` is assignable to it.
1316
- *
1317
- * @example
1318
- * ```ts
1319
- * const ctx: AnyNodeContext = { input, player, session, rng, fx, out, signal, now };
1320
- * ```
1321
- */
1322
- type AnyNodeContext = {
1323
- input: unknown;
1324
- player: unknown;
1325
- session: unknown;
1326
- rng: RngView;
1327
- fx: NodeFx;
1328
- out: Readonly<Record<string, (payload: never) => Result>>;
1329
- signal: AbortSignal;
1330
- now: number;
1331
- };
1332
- /**
1333
- * Any node as the runner holds it. `run` is a method on purpose: method parameters are compared
1334
- * bivariantly, so every typed `NodeDefinition` fits and the runner calls the body without a cast.
1335
- *
1336
- * @example
1337
- * ```ts
1338
- * const nodes: readonly AnyNode[] = [catchUp, merge];
1339
- * ```
1340
- */
1341
- type AnyNode = AnyWired & {
1342
- readonly kind: "node";
1343
- readonly rest: boolean;
1344
- readonly over: boolean;
1345
- readonly checkpoint: boolean;
1346
- readonly barrier: boolean;
1347
- readonly inbox: readonly string[];
1348
- run?(ctx: AnyNodeContext): Result | Promise<Result>;
1349
- };
1350
- /**
1351
- * One entry of a flow's `nodes`: a node, a sub-flow or a slot. The `kind` field tells them apart.
1352
- *
1353
- * @example
1354
- * ```ts
1355
- * const entry: FlowEntry | undefined = flow.nodes[name];
1356
- * ```
1357
- */
1358
- type FlowEntry = AnyNode | AnyFlow | SlotNode;
1359
- /**
1360
- * Any flow as the runner holds it. Every typed `FlowDefinition` fits.
1361
- *
1362
- * @example
1363
- * ```ts
1364
- * const flows: Map<string, AnyFlow> = collectFlows(mainFlow, []);
1365
- * ```
1366
- */
1367
- type AnyFlow = AnyWired & {
1368
- readonly kind: "flow";
1369
- readonly id: string;
1370
- readonly nodes: Readonly<Record<string, FlowEntry>>;
1371
- readonly start: string;
1372
- readonly edges: Readonly<Record<string, Readonly<Record<string, Target>>>>;
1373
- };
1374
- /**
1375
- * One level of the position. The path is the frames joined: `"board/awaitIntent"`.
1376
- *
1377
- * @example
1378
- * ```ts
1379
- * const frame: Frame = { flow: "board", node: "awaitIntent", input: null };
1380
- * ```
1381
- */
1382
- type Frame = {
1383
- flow: string;
1384
- node: string;
1385
- input: Json;
1386
- };
1387
- /**
1388
- * One taken edge. `hash` covers `path + outcome + next`, so a replay against edited code fails
1389
- * loudly in dev.
1390
- *
1391
- * @example
1392
- * ```ts
1393
- * const [last]: readonly JournalEntry[] = app.flow.history().slice(-1);
1394
- * ```
1395
- */
1396
- type JournalEntry = {
1397
- index: number;
1398
- path: string;
1399
- outcome: string;
1400
- payload: Json;
1401
- next: string;
1402
- now: number;
1403
- hash: string;
1404
- };
1405
- /**
1406
- * One step of a fast walk: a player answer at a rest node, or a substituted sub-flow result.
1407
- *
1408
- * @example
1409
- * ```ts
1410
- * const route: RouteStep[] = [
1411
- * { at: "home", intent: "play" },
1412
- * { at: "level", result: { outcome: "win", payload: { stars: 3 } } }
1413
- * ];
1414
- * ```
1415
- */
1416
- type RouteStep = {
1417
- at: string;
1418
- intent: string;
1419
- payload?: Json;
1420
- } | {
1421
- at: string;
1422
- result: {
1423
- outcome: string;
1424
- payload?: Json;
1425
- };
1426
- };
1427
- /**
1428
- * A rest node plus a state that really existed there. Serialisable. `graph` is the hash of
1429
- * `describe()`.
1430
- *
1431
- * @example
1432
- * ```ts
1433
- * const bookmark: Bookmark = app.flow.bookmark();
1434
- * await app.flow.restore(bookmark);
1435
- * ```
1436
- */
1437
- type Bookmark = {
1438
- path: string;
1439
- input: Json;
1440
- player: Json;
1441
- session: Json;
1442
- rng: RngState;
1443
- graph: string;
1444
- };
1445
- /**
1446
- * What `onEnter` callbacks learn about the node being entered.
1447
- *
1448
- * @example
1449
- * ```ts
1450
- * app.flow.onEnter("load", (node: NodeInfo) => preload(node.path));
1451
- * ```
1452
- */
1453
- type NodeInfo = {
1454
- path: string;
1455
- flow: string;
1456
- node: string;
1457
- rest: boolean;
1458
- over: boolean;
1459
- checkpoint: boolean;
1460
- barrier: boolean;
1461
- };
1462
- /**
1463
- * The whole graph as JSON. Edge targets are rendered as strings: `"node"`, `"exit:win"`,
1464
- * `"map:node"`.
1465
- *
1466
- * @example
1467
- * ```ts
1468
- * const graph: FlowGraph = app.flow.describe();
1469
- * graph.flows[graph.main]?.start;
1470
- * ```
1471
- */
1472
- type FlowGraph = {
1473
- main: string;
1474
- flows: Record<string, {
1475
- nodes: Record<string, GraphNode>;
1476
- start: string;
1477
- edges: Record<string, Record<string, string>>;
1478
- }>;
1479
- slots: Record<string, {
1480
- feature: string;
1481
- flow: string;
1482
- order: number;
1483
- }[]>;
1484
- };
1485
- /**
1486
- * One node of `describe()`: its flags, its outcome names and, when it is one, the slot it opens,
1487
- * the sub-flow it enters and the feature that brought it.
1488
- *
1489
- * @example
1490
- * ```ts
1491
- * const node: GraphNode | undefined = app.flow.describe().flows.main?.nodes.board;
1492
- * ```
1493
- */
1494
- type GraphNode = NodeInfo & {
1495
- outcomes: string[];
1496
- slot?: string;
1497
- subFlow?: string;
1498
- owner?: string;
1499
- };
1500
- /**
1501
- * Inspection of the running graph.
1502
- *
1503
- * @example
1504
- * ```ts
1505
- * const { path, pending }: FlowState = app.flow.state();
1506
- * ```
1507
- */
1508
- type FlowState = {
1509
- running: boolean;
1510
- path: string;
1511
- stack: readonly Frame[];
1512
- pending: {
1513
- fx?: string;
1514
- gate?: readonly string[];
1515
- };
1516
- mode: "live" | "fast";
1517
- };
1518
- /**
1519
- * Stages of entering a node: `assets` preloads at `load`, `scenes` switches at `scene`.
1520
- *
1521
- * @example
1522
- * ```ts
1523
- * const stage: Stage = "load";
1524
- * ```
1525
- */
1526
- type Stage = "load" | "scene";
1527
- /**
1528
- * Callback of `onEnter`. Called before `node.run`, awaited.
1529
- *
1530
- * @example
1531
- * ```ts
1532
- * const preloadNode: EnterCallback = (node, { signal }) => assets.preload(node.path, signal);
1533
- * ```
1534
- */
1535
- type EnterCallback = (node: NodeInfo, ctx: {
1536
- mode: "live" | "fast";
1537
- signal: AbortSignal;
1538
- }) => void | Promise<void>;
1539
- /**
1540
- * The seam the fast walk and `restore` steer the running loop with. It stays absent while the
1541
- * game just runs: `walk.ts` and `restore` create it through `loopSeam` when they need it.
1542
- *
1543
- * @example
1544
- * ```ts
1545
- * loopSeam(ctx.state.runner).substitutions.set("level", { outcome: "win", payload: null });
1546
- * ```
1547
- */
1548
- type LoopSeam = {
1549
- /** 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. */
1550
- rest: ((path: string) => void)[]; /** Called every time the loop opens the gate of a rest node. The walk waits on it. */
1551
- gateOpen: (() => void)[]; /** The bookmark the loop enters at the next turn. */
1552
- restoring: Bookmark | undefined;
1553
- };
1554
- /**
1555
- * runner module state.
1556
- *
1557
- * @example
1558
- * ```ts
1559
- * const runner: RunnerState = createRunnerState();
1560
- * ```
1561
- */
1562
- type RunnerState = {
1563
- /** Flush started by a background pause. `onStop` awaits it. */flushing: Promise<void> | undefined; /** Every reachable or registered flow by id. */
1564
- flows: Map<string, AnyFlow>;
1565
- enterCallbacks: Record<Stage, EnterCallback[]>; /** The position: one frame per nesting level. */
1566
- stack: Frame[]; /** The stack of the last rest node: the rollback target. */
1567
- restFrame: Frame[] | undefined; /** Edges since the last checkpoint. */
1568
- journal: JournalEntry[];
1569
- journalIndex: number; /** The promise of `run()`. `undefined`: not started. */
1570
- running: Promise<void> | undefined; /** Aborts the active node. */
1571
- abort: AbortController | undefined; /** Failed transitions in a row. */
1572
- failures: number; /** How `walk` and `restore` steer the loop. Absent until one of them needs it. */
1573
- seam?: LoopSeam;
1574
- };
1575
- /**
1576
- * runner module API. Its methods are spread onto the plugin root: `app.flow.run()`.
1577
- *
1578
- * @example
1579
- * ```ts
1580
- * createApp({ onStart: ctx => { ctx.flow.run().catch(showFatal); } });
1581
- * ```
1582
- */
1583
- type RunnerApi = {
1584
- run(): Promise<void>;
1585
- register(flow: AnyFlow): void;
1586
- onEnter(stage: Stage, callback: EnterCallback): () => void;
1587
- walk(route: readonly RouteStep[], options?: {
1588
- from?: Bookmark;
1589
- }): Promise<FlowState>;
1590
- bookmark(): Bookmark;
1591
- restore(bookmark: Bookmark): Promise<void>;
1592
- describe(): FlowGraph;
1593
- state(): FlowState;
1594
- history(): readonly JournalEntry[];
1595
- setMode(mode: "live" | "fast"): void;
1596
- };
1597
- //#endregion
1598
- //#region src/plugins/flow/features/types.d.ts
1599
- /**
1600
- * What a feature brings to the game. V1 keys only; keys of later milestones pass through the
1601
- * index signature until their plugin types them.
1602
- *
1603
- * @example
1604
- * ```ts
1605
- * const description: FeatureDescription = {
1606
- * flows: [rewardFlow],
1607
- * contribute: { afterWin: { flow: rewardFlow, order: 10 } }
1608
- * };
1609
- * ```
1610
- */
1611
- type FeatureDescription = {
1612
- /** Nodes the feature owns. Recorded for `describe()` and hot swap. */nodes?: readonly AnyNode[]; /** Flows the feature owns. */
1613
- flows?: readonly AnyFlow[]; /** Slot name to the sub-flow run in that slot. */
1614
- contribute?: Record<string, {
1615
- flow: AnyFlow;
1616
- order: number;
1617
- when?: (snapshot: Snapshot) => boolean;
1618
- }>;
1619
- [later: string]: unknown;
1620
- };
1621
- /**
1622
- * One sub-flow contributed to a slot.
1623
- *
1624
- * @example
1625
- * ```ts
1626
- * const [first]: readonly Contribution[] = app.flow.features.contributions("afterWin");
1627
- * ```
1628
- */
1629
- type Contribution = {
1630
- feature: string;
1631
- flow: AnyFlow;
1632
- order: number;
1633
- when?: (snapshot: Snapshot) => boolean;
1634
- };
1635
- /**
1636
- * features module state.
1637
- *
1638
- * @example
1639
- * ```ts
1640
- * const features: FeaturesState = createFeaturesState();
1641
- * ```
1642
- */
1643
- type FeaturesState = {
1644
- byName: Map<string, FeatureDescription>; /** True after `run()`: `register` throws. */
1645
- sealed: boolean;
1646
- };
1647
- /**
1648
- * features module API.
1649
- *
1650
- * @example
1651
- * ```ts
1652
- * ctx.require(flowPlugin).features.register("board", description);
1653
- * ```
1654
- */
1655
- type FeaturesApi = {
1656
- register(name: string, description: FeatureDescription): void;
1657
- all(): readonly {
1658
- name: string;
1659
- description: FeatureDescription;
1660
- }[];
1661
- contributions(slotName: string): readonly Contribution[];
1662
- };
1663
- //#endregion
1664
- //#region src/plugins/flow/runner/define.d.ts
1665
- /**
1666
- * Creates a type tag: a value that carries only a payload type. Without a type argument the
1667
- * payload is `void`. The tag never infers its type from the place it is written in.
1668
- *
1669
- * @returns A tag that carries the payload type and no value.
1670
- * @example
1671
- * ```ts
1672
- * const outcomes = { done: type(), orderComplete: type<{ rewardId: string }>() };
1673
- * ```
1674
- */
1675
- declare function type<Payload = void>(): TypeTag<NoInfer<Payload>>;
1676
- /**
1677
- * Defines a flow. The edge table is inferred as written and checked entry by entry: a missing
1678
- * edge, an unknown target and a payload that does not fit are compile errors with a sentence.
1679
- *
1680
- * @param id - Flow id, unique in the game.
1681
- * @param spec - Nodes, start node, edge table, and input and outcomes when used as a node.
1682
- * @returns The flow as plain data.
1683
- * @example
1684
- * ```ts
1685
- * const mainFlow = defineFlow("main", {
1686
- * nodes: { boot, home },
1687
- * start: "boot",
1688
- * edges: { boot: { done: "home" }, home: { play: "boot" } }
1689
- * });
1690
- * ```
1691
- */
1692
- 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>;
1693
- /**
1694
- * Edge target: leave the sub-flow with this outcome. The payload passes through to the parent.
1695
- *
1696
- * @param outcome - Outcome declared by the flow.
1697
- * @returns The exit target as plain data.
1698
- * @example
1699
- * ```ts
1700
- * edges: { play: { win: exit("win"), lose: exit("lose") } }
1701
- * ```
1702
- */
1703
- declare function exit<const Name extends string>(outcome: Name): Exit<Name>;
1704
- /**
1705
- * Edge target: adapt the payload on the edge. The mapper's parameter is annotated by the author;
1706
- * the annotation is checked against the outcome payload, the result against the target's input.
1707
- *
1708
- * @param target - Name of a node of the same flow.
1709
- * @param map - Pure function from the outcome payload to the target's input.
1710
- * @returns The mapped target as plain data.
1711
- * @example
1712
- * ```ts
1713
- * edges: { loadCore: { failed: to("retry", (failure: { reason: string }) => ({ why: failure.reason })) } }
1714
- * ```
1715
- */
1716
- declare function to<const TargetName extends string, Payload, Output>(target: TargetName, map: (payload: Payload) => Output): Mapped<TargetName, Payload, Output>;
1717
- /**
1718
- * Defines an extension point: a node whose body is "run the contributions of this slot in order".
1719
- * Its single outcome is `done`.
1720
- *
1721
- * @param name - Slot name features contribute to.
1722
- * @returns The slot node as plain data.
1723
- * @example
1724
- * ```ts
1725
- * const afterWin = slot("afterWin");
1726
- * ```
1727
- */
1728
- declare function slot(name: string): SlotNode;
1729
- declare namespace types_d_exports {
1730
- export { Allow, Answer, AnyFlow, AnyNode, AnyNodeContext, Api, Bookmark, CheckedEdges, Config, Contribution, DefineNode, Deps, Descriptor, Edges, EnterCallback, Events, Exit, FeatureDescription, FeaturePlugin, FeaturesApi, FlowCtx, FlowDefinition, FlowGraph, FlowSpec, FlowState, FxApi, FxHandler, GameState, GameTypes, GateApi, GraphError, GuideOptions, Hint, InboxApi, JournalEntry, KernelSlice, Kit, LifecycleChanged, Mapped, NodeContext, NodeDefinition, NodeFx, NodeInfo, NodeSpec, OutcomeTags, Result, RouteStep, RunnerApi, SlotNode, Stage, State, Target, TypeTag, WorldEvent };
1731
- }
1732
- /**
1733
- * flow plugin events.
1734
- *
1735
- * @example
1736
- * ```ts
1737
- * hooks: { "flow:rest": ({ path, checkpoint }) => track(path, checkpoint) }
1738
- * ```
1739
- */
1740
- type Events = {
1741
- /** An edge was taken and its state committed. */"flow:edge": {
1742
- flow: string;
1743
- node: string;
1744
- outcome: string;
1745
- payload: Json;
1746
- next: string;
1747
- patches: {
1748
- doc: Patch[];
1749
- session: Patch[];
1750
- };
1751
- index: number;
1752
- now: number;
1753
- }; /** The graph reached a rest node. */
1754
- "flow:rest": {
1755
- path: string;
1756
- checkpoint: boolean;
1757
- }; /** A node failed and the graph rolled back. */
1758
- "flow:error": {
1759
- path: string;
1760
- error: unknown;
1761
- rolledBackTo: string;
1762
- retry: boolean;
1763
- };
1764
- };
1765
- /**
1766
- * flow plugin config.
1767
- *
1768
- * @example
1769
- * ```ts
1770
- * createApp({ pluginConfigs: { flow: { mainFlow, safeNode: "home" } } });
1771
- * ```
1772
- */
1773
- type Config = {
1774
- /** 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`. */
1775
- safeNode: string | undefined; /** Retries of a failed transition before going to `safeNode`. */
1776
- retries: number; /** How long `onStop` waits for the active node to settle after abort, in real milliseconds. */
1777
- settleTimeoutMs: number; /** Journal entries kept between checkpoints. */
1778
- journalLimit: number;
1779
- };
1780
- /**
1781
- * flow plugin state: one branch per module.
1782
- *
1783
- * @example
1784
- * ```ts
1785
- * const state: State = createFlowState({ global, config });
1786
- * ```
1787
- */
1788
- type State = {
1789
- features: FeaturesState;
1790
- fx: FxState;
1791
- gate: GateState;
1792
- inbox: InboxState;
1793
- runner: RunnerState;
1794
- };
1795
- /**
1796
- * flow plugin API: the runner on the root, the other modules grouped.
1797
- *
1798
- * @example
1799
- * ```ts
1800
- * const flow: Api = ctx.require(flowPlugin);
1801
- * flow.gate.answer({ intent: "play" });
1802
- * ```
1803
- */
1804
- type Api = RunnerApi & {
1805
- gate: GateApi;
1806
- inbox: InboxApi;
1807
- fx: FxApi;
1808
- features: FeaturesApi;
1809
- };
1810
- /**
1811
- * Resolved dependency APIs.
1812
- *
1813
- * @example
1814
- * ```ts
1815
- * const deps: Deps = resolveDeps(ctx);
1816
- * deps.clock.now();
1817
- * ```
1818
- */
1819
- type Deps = {
1820
- time: Api$4;
1821
- model: Api$2;
1822
- clock: Api$1;
1823
- };
1824
- /**
1825
- * What the kernel context offers before the deps are attached.
1826
- * `emit` is one plain method overload per event on purpose: `flow` has dependencies with events,
1827
- * and both a property-typed and a generic `emit` break the kernel's event inference when a
1828
- * factory is passed to `createPlugin` by direct reference (`api`, `hooks`, `onInit`, `onStart`).
1829
- *
1830
- * @example
1831
- * ```ts
1832
- * export function createFlowApi(ctx: KernelSlice): Api;
1833
- * ```
1834
- */
1835
- type KernelSlice = Omit<PluginCtx<Config, State, Events>, "emit"> & {
1836
- emit(name: "flow:edge", payload: Events["flow:edge"]): void;
1837
- emit(name: "flow:rest", payload: Events["flow:rest"]): void;
1838
- emit(name: "flow:error", payload: Events["flow:error"]): void;
1839
- readonly global: object;
1840
- readonly log: Log.LogApi;
1841
- readonly require: Require;
1842
- };
1843
- /**
1844
- * Domain context shared by the modules.
1845
- *
1846
- * @example
1847
- * ```ts
1848
- * const flowCtx: FlowCtx = { ...ctx, deps: resolveDeps(ctx) };
1849
- * ```
1850
- */
1851
- type FlowCtx = KernelSlice & {
1852
- readonly deps: Deps;
1853
- };
1854
- /**
1855
- * Payload of the one event flow listens to.
1856
- *
1857
- * @example
1858
- * ```ts
1859
- * const onChanged = (payload: LifecycleChanged) => payload.resumed;
1860
- * ```
1861
- */
1862
- type LifecycleChanged = Events$2["lifecycle:changed"];
1863
- /**
1864
- * The types of one game. `player` and `session` type the node context; `assets` and `strings`
1865
- * are accepted now and used from later milestones.
1866
- *
1867
- * @example
1868
- * ```ts
1869
- * type Types = { player: Player; session: Session; assets: AssetKey; strings: StringTable };
1870
- * ```
1871
- */
1872
- type GameTypes = {
1873
- player: Json;
1874
- session: Json;
1875
- assets: string;
1876
- strings: Record<string, unknown>;
1877
- };
1878
- /**
1879
- * A feature is an ordinary plugin with no API of its own; `AnyPluginInstance` is the kernel's
1880
- * widened type for plugin lists. `logicOnly` is the same plugin reduced to the V1 keys.
1881
- *
1882
- * @example
1883
- * ```ts
1884
- * createApp({ plugins: [boardFeature.logicOnly] });
1885
- * ```
1886
- */
1887
- type FeaturePlugin = AnyPluginInstance & {
1888
- readonly logicOnly: AnyPluginInstance;
1889
- };
1890
- /**
1891
- * The flow helpers returned by `defineGame`. `defineNode` sees `player` and `session` with the
1892
- * game's types; at run time they are the same functions the plugin exports.
1893
- *
1894
- * @example
1895
- * ```ts
1896
- * const kit: Kit<Types> = defineGame<Types>();
1897
- * ```
1898
- */
1899
- type Kit<Types extends GameTypes> = {
1900
- defineNode: DefineNode<{
1901
- player: Types["player"];
1902
- session: Types["session"];
1903
- }>;
1904
- defineFlow: typeof defineFlow;
1905
- defineFeature: (name: string, description: FeatureDescription) => FeaturePlugin;
1906
- };
1907
- //#endregion
1908
- export { Patch as A, PauseReason as B, State$1 as C, Root as D, Json as E, StoreApi as F, State$4 as G, types_d_exports$3 as H, RngApi as I, Events$3 as J, types_d_exports$4 as K, RngState as L, ProviderCall as M, SaveDoc as N, State$2 as O, SaveUnreadableError as P, Api$3 as R, FakeClock as S, Config$2 as T, Api$4 as U, State$3 as V, Config$4 as W, GuideOptions as _, Kit as a, Api$1 as b, exit as c, type as d, FeatureDescription as f, Descriptor as g, RouteStep as h, GameTypes as i, PlayerStateProvider as j, types_d_exports$2 as k, slot as l, JournalEntry as m, Config as n, State as o, FlowState as p, Config$5 as q, FeaturePlugin as r, types_d_exports as s, Api as t, to as u, Hint as v, types_d_exports$1 as w, Config$1 as x, Answer as y, Config$3 as z };