@moku-labs/game 0.13.2 → 0.13.3

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.
Files changed (34) hide show
  1. package/dist/app/page.d.mts +1 -1
  2. package/dist/app/system.d.mts +3 -3
  3. package/dist/app.d.mts +2 -2
  4. package/dist/app.mjs +1 -1
  5. package/dist/assets.d.mts +1 -1
  6. package/dist/{capture-fpd83Mi5.mjs → capture-D7XvoHdH.mjs} +78 -1
  7. package/dist/{commands-BrgeMP5y.d.mts → commands-DRhJ_LIH.d.mts} +5 -5
  8. package/dist/control.d.mts +4 -4
  9. package/dist/control.mjs +1 -1
  10. package/dist/{define-ZmWbWxon.d.mts → define-Ckp70_GR.d.mts} +2 -2
  11. package/dist/{headless-BsNdJs9W.mjs → headless-BA6ffzqD.mjs} +30 -9
  12. package/dist/{headless-Dgxvp6Ym.d.mts → headless-CYgYzro0.d.mts} +1 -1
  13. package/dist/{index-CdbE_jGs.d.mts → index-CjuObHhF.d.mts} +4 -4
  14. package/dist/index.d.mts +5 -5
  15. package/dist/index.mjs +1 -1
  16. package/dist/inspect.d.mts +7 -7
  17. package/dist/jsx-dev-runtime.d.mts +1 -1
  18. package/dist/jsx-runtime.d.mts +1 -1
  19. package/dist/{memory-DJ2sPExv.d.mts → memory-DKo9rRWW.d.mts} +1 -1
  20. package/dist/{run-wYty_Lwo.mjs → run-BtyrJjmW.mjs} +130 -40
  21. package/dist/{runtime-6qakboTm.d.mts → runtime-iTxTcukZ.d.mts} +1 -1
  22. package/dist/{src-CltFFKtc.mjs → src-DlyYl_9X.mjs} +412 -122
  23. package/dist/testing.d.mts +4 -4
  24. package/dist/testing.mjs +2 -2
  25. package/dist/{types-C6y8EfEB.d.mts → types-BSMmnpFm.d.mts} +84 -24
  26. package/dist/{types-zRGJdFGX.d.mts → types-B_Bp2tWK.d.mts} +2 -2
  27. package/dist/{types-D57SntUt.d.mts → types-Bonifz5M.d.mts} +2 -2
  28. package/dist/{types-DGIkfZB7.d.mts → types-DkO3s9YX.d.mts} +4 -4
  29. package/dist/{types-CmuwAjkV.d.mts → types-Dm5Oh5bH.d.mts} +1 -1
  30. package/dist/{types-B3T2X6To.d.mts → types-dJMOXXL3.d.mts} +15 -11
  31. package/dist/visual.d.mts +6 -6
  32. package/dist/visual.mjs +1 -1
  33. package/llms.txt +4 -3
  34. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { r as systemSource, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { a as finishDrafts, i as enableDraftPatches, n as deepFreeze, o as openDrafts, r as dropDrafts } from "./drafts-BawpSaVp.mjs";
4
- import { c as findNode, d as slotNames, f as compact, i as servePaused, l as framePath, n as captureFrame, o as collectFlows, p as pushEntry, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as graphHash } from "./capture-fpd83Mi5.mjs";
4
+ import { d as graphHash, f as isRestCheckpoint, h as pushEntry, i as servePaused, l as findNode, m as compact, n as captureFrame, o as collectFlows, p as slotNames, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as framePath } from "./capture-D7XvoHdH.mjs";
5
5
  import { A as system, C as Exiting, D as component, E as Tree, T as Order, _ as Parent, a as visualOf$1, b as Transform, c as Text, g as NineSlice, h as Display, j as tag, k as resource, m as textFor, n as fitScaleOf, o as visualRectOf, p as readStyle, r as samePose, s as Countdown, t as CONTENT, u as builtInStyles, v as Shape, w as Layer, x as componentsFor, y as Sprite } from "./visual-dMW8iqAE.mjs";
6
6
  import { n as isDev } from "./dev-Cc50Dk7H.mjs";
7
7
  import { i as isComponentDefinition, n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
@@ -1933,19 +1933,6 @@ function connectFlow(ctx) {
1933
1933
  registerSchedule(flowCtx, fx);
1934
1934
  }
1935
1935
  //#endregion
1936
- //#region src/plugins/flow/runner/loop-types.ts
1937
- /** The stages of entering a node, in the order they run. */
1938
- const stages = ["load", "scene"];
1939
- /**
1940
- * Does nothing. It stands in for an unsubscribe function until the real one arrives.
1941
- *
1942
- * @example
1943
- * ```ts
1944
- * const watch = { off: noop };
1945
- * ```
1946
- */
1947
- function noop() {}
1948
- //#endregion
1949
1936
  //#region src/plugins/flow/runner/validate.ts
1950
1937
  /** Nodes of one flow above which `validate` warns. A flow holds 7 to 15 nodes. */
1951
1938
  const MAX_NODES = 15;
@@ -2400,6 +2387,16 @@ function collectGraph(ctx, features) {
2400
2387
  return graph.flows;
2401
2388
  }
2402
2389
  /**
2390
+ * Renders the whole graph as JSON. It reads the flows as data, so it works before `run()`.
2391
+ *
2392
+ * @param ctx - Domain context of the flow plugin.
2393
+ * @param features - Features API: owners and slot contributions.
2394
+ * @returns The graph.
2395
+ */
2396
+ function graphOf(ctx, features) {
2397
+ return describeGraph(collectGraph(ctx, features), features);
2398
+ }
2399
+ /**
2403
2400
  * Validates the collected graph. Warnings are logged, problems stop `run()` in one error.
2404
2401
  *
2405
2402
  * @param ctx - Domain context of the flow plugin.
@@ -2412,6 +2409,169 @@ function checkGraph(ctx, features) {
2412
2409
  if (report.problems.length > 0) throw new Error(report.problems.join("\n"));
2413
2410
  }
2414
2411
  //#endregion
2412
+ //#region src/plugins/flow/runner/bookmark.ts
2413
+ /**
2414
+ * Reads the main flow of the config.
2415
+ *
2416
+ * @param ctx - Domain context of the flow plugin.
2417
+ * @param method - Name of the API method that needs it, for the message.
2418
+ * @returns The top-level flow.
2419
+ * @throws {Error} When no main flow was configured.
2420
+ */
2421
+ function mainFlow(ctx, method) {
2422
+ const main = ctx.config.mainFlow;
2423
+ if (main === void 0) throw new Error(`[game] flow.${method}() needs a main flow.\n Pass it as pluginConfigs.flow.mainFlow.`);
2424
+ return main;
2425
+ }
2426
+ /**
2427
+ * Names a checkpoint of the graph, for the message of a refused bookmark. Only a rest node is
2428
+ * one: a transit node that carries the flag is no place a restore may be sent to.
2429
+ *
2430
+ * @param ctx - Domain context of the flow plugin.
2431
+ * @param modules - Injected sibling APIs.
2432
+ * @returns The path of the first rest checkpoint, or the start of the main flow.
2433
+ */
2434
+ function someCheckpoint(ctx, modules) {
2435
+ const main = mainFlow(ctx, "restore");
2436
+ for (const flow of collectGraph(ctx, modules.features).values()) for (const [name, entry] of Object.entries(flow.nodes)) if (isRestCheckpoint(entry)) return flow.id === main.id ? name : `${flow.id}/${name}`;
2437
+ return main.start;
2438
+ }
2439
+ /**
2440
+ * Looks up the node at a path. A sub-flow and a slot are no nodes: nothing runs at them.
2441
+ *
2442
+ * @param ctx - Domain context of the flow plugin.
2443
+ * @param modules - Injected sibling APIs.
2444
+ * @param path - Path of a bookmark, or of its rest point.
2445
+ * @returns The node there, a rest node or a transit node, or `undefined` when the path names none.
2446
+ */
2447
+ function nodeAt(ctx, modules, path) {
2448
+ const entry = findNode(mainFlow(ctx, "restore"), path, modules.features.contributions)?.entry;
2449
+ return entry?.kind === "node" ? entry : void 0;
2450
+ }
2451
+ /**
2452
+ * Finds the node a bookmark names.
2453
+ *
2454
+ * @param ctx - Domain context of the flow plugin.
2455
+ * @param modules - Injected sibling APIs.
2456
+ * @param bookmark - The bookmark to enter.
2457
+ * @returns The node at the bookmark's path, a rest node or a transit node.
2458
+ * @throws {Error} When the path names no node of the graph.
2459
+ */
2460
+ function bookmarkNode(ctx, modules, bookmark) {
2461
+ const node = nodeAt(ctx, modules, bookmark.path);
2462
+ if (node === void 0) throw new Error(`[game] The bookmark "${bookmark.path}" is not a node of the graph.\n Restore a rest node, for example the checkpoint "${someCheckpoint(ctx, modules)}".`);
2463
+ return node;
2464
+ }
2465
+ /**
2466
+ * Tells whether the graph is the one a bookmark was made for. The answer costs a description and
2467
+ * a hash of the whole graph, so one acceptance works it out once: see `askedOnce`.
2468
+ *
2469
+ * @param ctx - Domain context of the flow plugin.
2470
+ * @param modules - Injected sibling APIs.
2471
+ * @param bookmark - The bookmark to compare with.
2472
+ * @returns True while the hash of `describe()` is the bookmark's.
2473
+ */
2474
+ function sameGraph(ctx, modules, bookmark) {
2475
+ return graphHash(graphOf(ctx, modules.features)) === bookmark.graph;
2476
+ }
2477
+ /**
2478
+ * Makes a question that is worked out at most once: at the first ask, and never when nobody asks.
2479
+ *
2480
+ * @param ask - Works the answer out.
2481
+ * @returns The question. Every ask after the first gets the first answer.
2482
+ * @example
2483
+ * ```ts
2484
+ * const same = askedOnce(() => true);
2485
+ * same() && same(); // true, and the answer was worked out once
2486
+ * ```
2487
+ */
2488
+ function askedOnce(ask) {
2489
+ const answer = { value: void 0 };
2490
+ return () => {
2491
+ answer.value ??= ask();
2492
+ return answer.value;
2493
+ };
2494
+ }
2495
+ /**
2496
+ * Checks that a bookmark may be entered: a rest checkpoint always, any other node only while the
2497
+ * graph is the one the bookmark was made for. A transit node is never taken for a checkpoint,
2498
+ * whatever its flag says: it is a place inside a transition, not a place a save may stand on.
2499
+ *
2500
+ * @param ctx - Domain context of the flow plugin.
2501
+ * @param modules - Injected sibling APIs.
2502
+ * @param bookmark - The bookmark to enter.
2503
+ * @param unchanged - Asks whether the graph is the bookmark's own. Not asked for a checkpoint.
2504
+ * @returns The node the bookmark names.
2505
+ * @throws {Error} When the path is no node, or the graph changed since the bookmark.
2506
+ */
2507
+ function checkBookmark(ctx, modules, bookmark, unchanged) {
2508
+ const node = bookmarkNode(ctx, modules, bookmark);
2509
+ if (isRestCheckpoint(node)) return node;
2510
+ if (unchanged()) return node;
2511
+ throw new Error(`[game] The bookmark "${bookmark.path}" was made for another graph.\n Restore the checkpoint "${someCheckpoint(ctx, modules)}" instead.`);
2512
+ }
2513
+ /**
2514
+ * Builds the bookmark of the rest point before a transit node, for a bookmark that carries `rest`
2515
+ * and was made for another graph. That is one whose path names a transit node while the hash
2516
+ * differs, and one whose path names no node any more: a renamed or removed node is a changed
2517
+ * graph too. Only the position changes: the state stays the bookmark's.
2518
+ *
2519
+ * @param ctx - Domain context of the flow plugin.
2520
+ * @param modules - Injected sibling APIs.
2521
+ * @param bookmark - The bookmark to enter.
2522
+ * @param unchanged - Asks whether the graph is the bookmark's own. Asked for a transit node only.
2523
+ * @returns The bookmark to enter instead, or `undefined` when the bookmark stands for itself.
2524
+ */
2525
+ function restFallback(ctx, modules, bookmark, unchanged) {
2526
+ const rest = bookmark.rest;
2527
+ if (rest === void 0) return void 0;
2528
+ const node = nodeAt(ctx, modules, bookmark.path);
2529
+ if (node !== void 0 && (node.rest || unchanged())) return void 0;
2530
+ return {
2531
+ ...bookmark,
2532
+ path: rest.path,
2533
+ input: rest.input
2534
+ };
2535
+ }
2536
+ /**
2537
+ * Decides what the loop enters for a bookmark: the bookmark itself, or the rest point before it
2538
+ * when the graph changed under a bookmark that has one. Either one passes the same check, so the
2539
+ * fallback is entered only when it is a checkpoint, and the log says that it was taken.
2540
+ *
2541
+ * @param ctx - Domain context of the flow plugin.
2542
+ * @param modules - Injected sibling APIs.
2543
+ * @param bookmark - The bookmark to enter.
2544
+ * @returns The bookmark the loop enters, and whether its node is a rest node.
2545
+ * @throws {Error} When neither the bookmark nor its rest point may be entered.
2546
+ */
2547
+ function acceptBookmark(ctx, modules, bookmark) {
2548
+ const unchanged = askedOnce(() => sameGraph(ctx, modules, bookmark));
2549
+ const fallback = restFallback(ctx, modules, bookmark, unchanged);
2550
+ const entered = fallback ?? bookmark;
2551
+ const node = checkBookmark(ctx, modules, entered, unchanged);
2552
+ if (fallback !== void 0) ctx.log.info("flow:restore-fell-back", {
2553
+ from: bookmark.path,
2554
+ to: fallback.path
2555
+ });
2556
+ return {
2557
+ bookmark: entered,
2558
+ rest: node.rest
2559
+ };
2560
+ }
2561
+ //#endregion
2562
+ //#region src/plugins/flow/runner/loop-types.ts
2563
+ /** The stages of entering a node, in the order they run. */
2564
+ const stages = ["load", "scene"];
2565
+ /**
2566
+ * Does nothing. It stands in for an unsubscribe function until the real one arrives.
2567
+ *
2568
+ * @example
2569
+ * ```ts
2570
+ * const watch = { off: noop };
2571
+ * ```
2572
+ */
2573
+ function noop() {}
2574
+ //#endregion
2415
2575
  //#region src/plugins/flow/runner/position.ts
2416
2576
  /**
2417
2577
  * Resolves what the deepest frame points at.
@@ -3075,8 +3235,27 @@ function guideAllow(payload) {
3075
3235
  };
3076
3236
  }
3077
3237
  /**
3238
+ * Refuses an effect a body starts after its node was aborted. The rejection counts as handled
3239
+ * from the start: a body that does not await the effect leaves no unhandled rejection behind,
3240
+ * and a body that awaits it still gets the reason.
3241
+ *
3242
+ * @param signal - The aborted signal of the node.
3243
+ * @returns A promise rejected with the abort reason.
3244
+ * @example
3245
+ * ```ts
3246
+ * refuseEffect(AbortSignal.abort("restore")); // a promise rejected with "restore"
3247
+ * ```
3248
+ */
3249
+ function refuseEffect(signal) {
3250
+ const refused = Promise.reject(abortReason(signal));
3251
+ refused.catch(noop);
3252
+ return refused;
3253
+ }
3254
+ /**
3078
3255
  * Builds the `fx` of a node context: awaited effects by call, cosmetic hints by `emit`. A `guide`
3079
- * narrows the gate while it runs; the runner lifts the narrow when the node is left.
3256
+ * narrows the gate while it runs; the runner lifts the narrow when the node is left. A body that
3257
+ * was aborted keeps running, since the loop only stops waiting for it, so its `fx` starts nothing
3258
+ * any more: the gate and the narrow belong to the node the loop entered after it.
3080
3259
  *
3081
3260
  * @param modules - Injected sibling APIs.
3082
3261
  * @param step - The node run in progress.
@@ -3084,12 +3263,13 @@ function guideAllow(payload) {
3084
3263
  */
3085
3264
  function nodeFx(modules, step) {
3086
3265
  /**
3087
- * Awaits one effect.
3266
+ * Awaits one effect. After the abort of the node it rejects with the abort reason instead.
3088
3267
  *
3089
3268
  * @param descriptor - The descriptor the node awaits.
3090
3269
  * @returns The answer, the handler's value, or `undefined`.
3091
3270
  */
3092
3271
  const run = (descriptor) => {
3272
+ if (step.signal.aborted) return refuseEffect(step.signal);
3093
3273
  if (descriptor.kind === "guide") {
3094
3274
  const allow = guideAllow(descriptor.payload);
3095
3275
  if (allow !== void 0) modules.gate.narrow(allow);
@@ -3599,19 +3779,23 @@ function enterSlot(ctx, modules, location, slot, safe) {
3599
3779
  return "continue";
3600
3780
  }
3601
3781
  /**
3602
- * Enters the node of a bookmark: the state is replaced, the rest point is marked and the position
3603
- * becomes the bookmark's path.
3782
+ * Enters the node of a bookmark, a rest node or a transit node: the state is replaced, the rest
3783
+ * point is marked and the position becomes the bookmark's path. The mark is made for a transit
3784
+ * node too. A restore leaves the whole document waiting for the provider, and a rollback of the
3785
+ * node would drop it unsent; and the frames of the transit node are what a failure of it must
3786
+ * retry, since the restored state belongs to them. `flow:rest` and the rest seam fire once for
3787
+ * every node entered here; only a rest node is announced as a checkpoint.
3604
3788
  *
3605
3789
  * @param ctx - Domain context of the flow plugin.
3606
3790
  * @param modules - Injected sibling APIs; the features API resolves a path through a slot.
3607
3791
  * @param bookmark - The bookmark to enter.
3608
3792
  * @throws {Error} When the graph has no node at the bookmark's path.
3609
3793
  */
3610
- function enterBookmark$1(ctx, modules, bookmark) {
3794
+ function enterBookmark(ctx, modules, bookmark) {
3611
3795
  const state = ctx.state.runner;
3612
3796
  const location = findNode(requireMainFlow(ctx), bookmark.path, modules.features.contributions);
3613
3797
  loopSeam(state).restoring = void 0;
3614
- if (location === void 0) throw new Error(`[game] The bookmark names no node "${bookmark.path}".\n Bookmark a rest node of the running graph.`);
3798
+ if (location === void 0) throw new Error(`[game] The bookmark names no node "${bookmark.path}".\n Bookmark a node of the running graph.`);
3615
3799
  ctx.deps.model.store.restore({
3616
3800
  player: bookmark.player,
3617
3801
  session: bookmark.session,
@@ -3622,10 +3806,9 @@ function enterBookmark$1(ctx, modules, bookmark) {
3622
3806
  state.slotAfter = void 0;
3623
3807
  state.restFrame = [...state.stack];
3624
3808
  state.failures = 0;
3625
- const entry = location.entry;
3626
3809
  ctx.emit("flow:rest", {
3627
3810
  path: bookmark.path,
3628
- checkpoint: entry.kind === "node" && entry.checkpoint
3811
+ checkpoint: isRestCheckpoint(location.entry)
3629
3812
  });
3630
3813
  notifyRest(state, bookmark.path);
3631
3814
  }
@@ -3643,7 +3826,7 @@ async function advance(ctx, modules, safe) {
3643
3826
  if (stopped(state)) return "stop";
3644
3827
  const restoring = state.seam?.restoring;
3645
3828
  if (restoring !== void 0) {
3646
- enterBookmark$1(ctx, modules, restoring);
3829
+ enterBookmark(ctx, modules, restoring);
3647
3830
  return "continue";
3648
3831
  }
3649
3832
  const location = locateFrames(ctx, state.stack);
@@ -3685,12 +3868,15 @@ async function runLoop(ctx, modules) {
3685
3868
  }
3686
3869
  /**
3687
3870
  * Restores a bookmark through the running loop: the active node is aborted with reason
3688
- * `"restore"`, its transaction is discarded, the state is replaced and the node is entered.
3871
+ * `"restore"`, its transaction is discarded, the state is replaced and the node is entered. An
3872
+ * edge that was already committed still arrives first, and the rest point it reaches is not the
3873
+ * bookmark's: the promise waits for the turn in which the loop takes the bookmark.
3689
3874
  *
3690
3875
  * @param ctx - Domain context of the flow plugin.
3691
3876
  * @param _modules - Injected sibling APIs.
3692
3877
  * @param bookmark - The bookmark to enter.
3693
- * @returns A promise that resolves once the loop rests at the bookmark's node.
3878
+ * @returns A promise that resolves once the loop entered the bookmark's node, before that node
3879
+ * ran: the rest seam fired.
3694
3880
  * @throws {Error} When the loop is not running.
3695
3881
  */
3696
3882
  function restorePosition(ctx, _modules, bookmark) {
@@ -3702,6 +3888,7 @@ function restorePosition(ctx, _modules, bookmark) {
3702
3888
  * Resolves once, when the loop rests at the restored node.
3703
3889
  */
3704
3890
  const listener = () => {
3891
+ if (seam.restoring !== void 0) return;
3705
3892
  const index = seam.rest.indexOf(listener);
3706
3893
  if (index !== -1) seam.rest.splice(index, 1);
3707
3894
  resolve();
@@ -3871,24 +4058,37 @@ function watchEnd(running) {
3871
4058
  return end;
3872
4059
  }
3873
4060
  /**
3874
- * Waits for the next gate the loop opens. The listener takes itself off when it fires, so a walk
3875
- * that ended meanwhile leaves nothing behind.
4061
+ * Watches for the next gate the loop opens. The listener leaves the seam when a gate wakes it. A
4062
+ * wait that something else ended calls `off()`: the gate that would take the listener off may
4063
+ * never come.
3876
4064
  *
3877
4065
  * @param seam - The seam of the running loop.
3878
- * @returns A promise that resolves when the loop opens a gate.
4066
+ * @returns The promise of the next open gate, and the way to stop watching.
3879
4067
  */
3880
4068
  function nextGateOpen(seam) {
3881
- return new Promise((resolve) => {
3882
- /**
3883
- * Wakes the walk once the loop opened the gate.
3884
- */
3885
- const listener = () => {
3886
- const index = seam.gateOpen.indexOf(listener);
3887
- if (index !== -1) seam.gateOpen.splice(index, 1);
3888
- resolve();
3889
- };
3890
- seam.gateOpen.push(listener);
4069
+ const watch = { wake: void 0 };
4070
+ /**
4071
+ * Takes the listener off the seam. It may be gone already.
4072
+ */
4073
+ const off = () => {
4074
+ const index = seam.gateOpen.indexOf(listener);
4075
+ if (index !== -1) seam.gateOpen.splice(index, 1);
4076
+ };
4077
+ /**
4078
+ * Wakes the waiting side once the loop opened a gate, and leaves.
4079
+ */
4080
+ function listener() {
4081
+ off();
4082
+ watch.wake?.();
4083
+ }
4084
+ const promise = new Promise((resolve) => {
4085
+ watch.wake = resolve;
3891
4086
  });
4087
+ seam.gateOpen.push(listener);
4088
+ return {
4089
+ promise,
4090
+ off
4091
+ };
3892
4092
  }
3893
4093
  /**
3894
4094
  * Waits until the loop opens its gate. Between the edge it commits and the gate of the rest node
@@ -3902,7 +4102,40 @@ function nextGateOpen(seam) {
3902
4102
  async function reachGate(ctx, seam, ended) {
3903
4103
  if (ctx.state.gate.open !== void 0) return;
3904
4104
  if (ended.done) return;
3905
- await Promise.race([nextGateOpen(seam), ended.settled]);
4105
+ const opened = nextGateOpen(seam);
4106
+ try {
4107
+ await Promise.race([opened.promise, ended.settled]);
4108
+ } finally {
4109
+ opened.off();
4110
+ }
4111
+ }
4112
+ /**
4113
+ * Waits until a transit node the loop was sent into shows what it waits for: the gate it opens,
4114
+ * or, when it opens none, the next rest point or the end of the loop. `restore` waits here after
4115
+ * it entered a transit bookmark, so its caller reads a state with the open gate in it. There is no
4116
+ * timeout: a node that opens no gate, never rests and never ends keeps the wait pending, as its
4117
+ * body would. Call it only after the loop entered the node: a gate opened before that belongs to
4118
+ * the node the restore aborted.
4119
+ *
4120
+ * @param ctx - Domain context of the flow plugin.
4121
+ * @returns A promise that resolves at the open gate, at a rest point or at the end of the loop.
4122
+ */
4123
+ async function reachGateOrRest(ctx) {
4124
+ const running = ctx.state.runner.running;
4125
+ if (running === void 0 || ctx.state.gate.open !== void 0) return;
4126
+ const seam = loopSeam(ctx.state.runner);
4127
+ const opened = nextGateOpen(seam);
4128
+ const rested = watchRest(seam);
4129
+ try {
4130
+ await Promise.race([
4131
+ opened.promise,
4132
+ rested.next(),
4133
+ watchEnd(running).settled
4134
+ ]);
4135
+ } finally {
4136
+ opened.off();
4137
+ rested.off();
4138
+ }
3906
4139
  }
3907
4140
  /**
3908
4141
  * The error of a step the loop never reached, naming where it stands instead.
@@ -4020,14 +4253,17 @@ async function playRoute(ctx, modules, route, running) {
4020
4253
  /**
4021
4254
  * Walks a route through the running loop: restores `from` when given, switches to fast mode,
4022
4255
  * waits until the loop rests at each step's `at` and answers through the gate, and substitutes
4023
- * the result of every sub-flow node the route skips. The mode of the caller is put back
4024
- * afterwards, also when a step rejects, and a substitution the route never reached is disarmed:
4025
- * it must not skip a sub-flow in the live play that follows.
4256
+ * the result of every sub-flow node the route skips. A rest node `from` names is entered before
4257
+ * the switch: the stages of that node see the mode of the caller. A transit node is entered
4258
+ * after the switch, so it never starts live and never shows its popup. The mode of the caller
4259
+ * is put back afterwards, also when the bookmark is refused or a step rejects, and a
4260
+ * substitution the route never reached is disarmed: it must not skip a sub-flow in the live play
4261
+ * that follows.
4026
4262
  *
4027
4263
  * @param ctx - Domain context of the flow plugin.
4028
4264
  * @param modules - Injected sibling APIs: features, fx, gate, inbox.
4029
4265
  * @param route - The player's answers and substituted results, in order.
4030
- * @param start - Bookmark to enter first and the checked restore that enters it.
4266
+ * @param start - Bookmark to enter first, the restore that enters it and the kind of its node.
4031
4267
  * @returns The state the walk ended in.
4032
4268
  * @throws {Error} Before `run()` was called, when the bookmark is refused, and when a step's `at`
4033
4269
  * is never reached.
@@ -4035,10 +4271,13 @@ async function playRoute(ctx, modules, route, running) {
4035
4271
  async function walkRoute(ctx, modules, route, start) {
4036
4272
  const running = ctx.state.runner.running;
4037
4273
  if (running === void 0) throw new Error("[game] flow.walk() needs a running graph.\n Call flow.run() from the createApp onStart callback first.");
4038
- if (start !== void 0) await start.restore(start.from);
4274
+ const entersBeforeSwitch = start !== void 0 && !start.transit;
4275
+ const entersAfterSwitch = start !== void 0 && !entersBeforeSwitch;
4276
+ if (entersBeforeSwitch) await start.restore(start.from);
4039
4277
  const previous = ctx.state.fx.mode;
4040
4278
  modules.fx.setMode("fast");
4041
4279
  try {
4280
+ if (entersAfterSwitch) await start.restore(start.from);
4042
4281
  await playRoute(ctx, modules, route, running);
4043
4282
  } finally {
4044
4283
  loopSeam(ctx.state.runner).substitutions.clear();
@@ -4050,69 +4289,71 @@ async function walkRoute(ctx, modules, route, start) {
4050
4289
  //#region src/plugins/flow/runner/api.ts
4051
4290
  const noPayload = null;
4052
4291
  /**
4053
- * Reads the main flow of the config.
4292
+ * Tells whether a frame points at a rest node.
4054
4293
  *
4055
4294
  * @param ctx - Domain context of the flow plugin.
4056
- * @param method - Name of the API method that needs it, for the message.
4057
- * @returns The top-level flow.
4058
- * @throws {Error} When no main flow was configured.
4295
+ * @param frame - One level of a position.
4296
+ * @returns True when the entry of that frame is a node with `rest`.
4059
4297
  */
4060
- function mainFlow(ctx, method) {
4061
- const main = ctx.config.mainFlow;
4062
- if (main === void 0) throw new Error(`[game] flow.${method}() needs a main flow.\n Pass it as pluginConfigs.flow.mainFlow.`);
4063
- return main;
4298
+ function restsAt(ctx, frame) {
4299
+ const entry = ctx.state.runner.flows.get(frame.flow)?.nodes[frame.node];
4300
+ return entry !== void 0 && entry.kind === "node" && entry.rest;
4064
4301
  }
4065
4302
  /**
4066
- * Renders the whole graph as JSON. It reads the flows as data, so it works before `run()`.
4303
+ * Tells whether the graph stands on a rest node. `setMode` is legal there and before `run()`.
4067
4304
  *
4068
4305
  * @param ctx - Domain context of the flow plugin.
4069
- * @param modules - Injected sibling APIs.
4070
- * @returns The graph.
4306
+ * @returns True while the position is empty or points at a rest node.
4071
4307
  */
4072
- function graphOf(ctx, modules) {
4073
- return describeGraph(collectGraph(ctx, modules.features), modules.features);
4308
+ function resting(ctx) {
4309
+ const frame = ctx.state.runner.stack.at(-1);
4310
+ return frame === void 0 || restsAt(ctx, frame);
4074
4311
  }
4075
4312
  /**
4076
- * Names a checkpoint of the graph, for the message of a refused bookmark.
4313
+ * Tells whether a transit node waits at the gate: an effect that takes answers is open inside
4314
+ * it. A rest node with its own gate open rests, so it does not count.
4077
4315
  *
4078
4316
  * @param ctx - Domain context of the flow plugin.
4079
- * @param modules - Injected sibling APIs.
4080
- * @returns The path of the first checkpoint, or the start of the main flow.
4317
+ * @returns True while the gate is open and the position is no rest node.
4081
4318
  */
4082
- function someCheckpoint(ctx, modules) {
4083
- const main = mainFlow(ctx, "restore");
4084
- for (const flow of collectGraph(ctx, modules.features).values()) for (const [name, entry] of Object.entries(flow.nodes)) if (entry.kind === "node" && entry.checkpoint) return flow.id === main.id ? name : `${flow.id}/${name}`;
4085
- return main.start;
4319
+ function waitsInTransit(ctx) {
4320
+ return ctx.state.gate.open !== void 0 && !resting(ctx);
4086
4321
  }
4087
4322
  /**
4088
- * Checks that a bookmark may be entered: a checkpoint always, any other rest node only while the
4089
- * graph is the one the bookmark was made for.
4323
+ * Reads the rest point before the transit node the graph stands in. The rest frame names a
4324
+ * transit node when the graph started at one or a restore entered one: no rest point came
4325
+ * before it then.
4090
4326
  *
4091
4327
  * @param ctx - Domain context of the flow plugin.
4092
- * @param modules - Injected sibling APIs.
4093
- * @param bookmark - The bookmark to enter.
4094
- * @throws {Error} When the path is no rest node, or the graph changed since the bookmark.
4328
+ * @returns The last rest node and its input, or `undefined` when the rest frame names none.
4095
4329
  */
4096
- function checkBookmark(ctx, modules, bookmark) {
4097
- const entry = findNode(mainFlow(ctx, "restore"), bookmark.path, modules.features.contributions)?.entry;
4098
- if (entry === void 0 || entry.kind !== "node" || !entry.rest) throw new Error(`[game] The bookmark "${bookmark.path}" is not a rest node of the graph.\n Restore a rest node, for example the checkpoint "${someCheckpoint(ctx, modules)}".`);
4099
- if (entry.checkpoint) return;
4100
- if (graphHash(graphOf(ctx, modules)) === bookmark.graph) return;
4101
- throw new Error(`[game] The bookmark "${bookmark.path}" was made for another graph.\n Restore the checkpoint "${someCheckpoint(ctx, modules)}" instead.`);
4330
+ function restBefore(ctx) {
4331
+ const frames = ctx.state.runner.restFrame ?? [];
4332
+ const frame = frames.at(-1);
4333
+ if (frame === void 0 || !restsAt(ctx, frame)) return void 0;
4334
+ return {
4335
+ path: framePath(frames),
4336
+ input: frame.input
4337
+ };
4102
4338
  }
4103
4339
  /**
4104
- * Makes a bookmark of the rest point the graph stands on.
4340
+ * Makes a bookmark of where the graph stands. While a transit node waits at the gate for the
4341
+ * answer of an effect it is that node, with the rest point before it; in every other moment it is
4342
+ * the last rest point. The state is the committed one in both cases: for a waiting transit node
4343
+ * that is the state it was entered with, since its own writes are still in the open transaction.
4105
4344
  *
4106
4345
  * @param ctx - Domain context of the flow plugin.
4107
4346
  * @param modules - Injected sibling APIs.
4108
- * @returns A rest node plus a state that really existed there.
4347
+ * @returns A node plus a state that really existed there.
4109
4348
  * @throws {Error} When the graph has no position yet.
4110
4349
  */
4111
4350
  function makeBookmark(ctx, modules) {
4112
4351
  const state = ctx.state.runner;
4113
- const frames = state.restFrame ?? state.stack;
4352
+ const waiting = waitsInTransit(ctx);
4353
+ const frames = waiting ? state.stack : state.restFrame ?? state.stack;
4114
4354
  if (frames.length === 0) throw new Error("[game] flow.bookmark() needs a position.\n Call it while the graph rests at a node.");
4115
4355
  const snapshot = ctx.deps.model.store.snapshot();
4356
+ const rest = waiting ? restBefore(ctx) : void 0;
4116
4357
  return {
4117
4358
  path: framePath(frames),
4118
4359
  input: frames.at(-1)?.input ?? noPayload,
@@ -4122,35 +4363,68 @@ function makeBookmark(ctx, modules) {
4122
4363
  seed: snapshot.rng.seed,
4123
4364
  streams: { ...snapshot.rng.streams }
4124
4365
  },
4125
- graph: graphHash(graphOf(ctx, modules))
4366
+ graph: graphHash(graphOf(ctx, modules.features)),
4367
+ ...rest === void 0 ? {} : { rest }
4126
4368
  };
4127
4369
  }
4128
4370
  /**
4129
- * Enters a bookmark the way the plugin's `restore` does: the bookmark is checked first, then the
4130
- * running loop is sent to its node. `walk({ from })` enters through here as well, so both ways
4131
- * into a position refuse the same bookmarks.
4371
+ * Sends the running loop to a bookmark that passed the check, the short way a walk needs: the
4372
+ * promise resolves as soon as the loop stands at the node, before the node ran. It stays an
4373
+ * `async` function of its own: the microtask turn it adds lets the loop start the stages of a
4374
+ * rest node before the walk goes on and switches to fast mode.
4132
4375
  *
4133
4376
  * @param ctx - Domain context of the flow plugin.
4134
4377
  * @param modules - Injected sibling APIs.
4135
- * @param bookmark - The bookmark to enter.
4136
- * @returns A promise that resolves once the loop rests at the bookmark's node.
4137
- * @throws {Error} When the bookmark names no rest node of this graph.
4378
+ * @param bookmark - The accepted bookmark.
4379
+ * @returns A promise that resolves once the loop entered the node.
4138
4380
  */
4139
- async function enterBookmark(ctx, modules, bookmark) {
4140
- checkBookmark(ctx, modules, bookmark);
4381
+ async function enterAccepted(ctx, modules, bookmark) {
4141
4382
  await restorePosition(ctx, modules, bookmark);
4142
4383
  }
4143
4384
  /**
4144
- * Tells whether the graph stands on a rest node. `setMode` is legal there and before `run()`.
4385
+ * Walks a route from a bookmark. The bookmark passes the check `restore` runs, so both ways into
4386
+ * a position refuse the same bookmarks and take the same fallback. The walk is told which kind of
4387
+ * node it enters: a rest node is entered before the switch to fast mode, a transit node after it.
4145
4388
  *
4146
4389
  * @param ctx - Domain context of the flow plugin.
4147
- * @returns True while the position is empty or points at a rest node.
4390
+ * @param modules - Injected sibling APIs.
4391
+ * @param route - The player's answers and substituted results, in order.
4392
+ * @param from - The bookmark to enter first.
4393
+ * @returns The state the walk ended in.
4394
+ * @throws {Error} When the bookmark may not be entered, and when a step's `at` is never reached.
4148
4395
  */
4149
- function resting(ctx) {
4150
- const frame = ctx.state.runner.stack.at(-1);
4151
- if (frame === void 0) return true;
4152
- const entry = ctx.state.runner.flows.get(frame.flow)?.nodes[frame.node];
4153
- return entry !== void 0 && entry.kind === "node" && entry.rest;
4396
+ async function walkFrom(ctx, modules, route, from) {
4397
+ const accepted = acceptBookmark(ctx, modules, from);
4398
+ return walkRoute(ctx, modules, route, {
4399
+ /**
4400
+ * Enters the accepted bookmark. The walk never waits for a gate here: it answers the gates
4401
+ * of its own route.
4402
+ *
4403
+ * @param bookmark - The bookmark to enter.
4404
+ * @returns A promise that resolves once the loop entered its node.
4405
+ */
4406
+ restore: (bookmark) => enterAccepted(ctx, modules, bookmark),
4407
+ from: accepted.bookmark,
4408
+ transit: !accepted.rest
4409
+ });
4410
+ }
4411
+ /**
4412
+ * Restores a bookmark, the way the plugin's `restore` does. A rest node is entered as `walk`
4413
+ * enters it. A transit node shows nothing until it ran up to its gate, so for one the promise
4414
+ * waits on: for the gate the node opens, or the rest point or the end of the loop it reaches
4415
+ * without one. The wait starts only after the loop entered the node, so a gate the aborted old
4416
+ * node still opened is not taken for the new one.
4417
+ *
4418
+ * @param ctx - Domain context of the flow plugin.
4419
+ * @param modules - Injected sibling APIs.
4420
+ * @param bookmark - The bookmark to enter.
4421
+ * @returns A promise that resolves once a rest node is entered, or a transit node waits.
4422
+ * @throws {Error} When the bookmark may not be entered, and before `run()`.
4423
+ */
4424
+ async function restoreBookmark(ctx, modules, bookmark) {
4425
+ const accepted = acceptBookmark(ctx, modules, bookmark);
4426
+ await restorePosition(ctx, modules, accepted.bookmark);
4427
+ if (!accepted.rest) await reachGateOrRest(ctx);
4154
4428
  }
4155
4429
  /**
4156
4430
  * Creates the runner API: `run` owns the one loop, `walk` and `restore` enter a position through
@@ -4179,21 +4453,12 @@ function createRunnerApi(ctx, modules) {
4179
4453
  },
4180
4454
  walk: (route, options) => {
4181
4455
  const from = options?.from;
4182
- if (from === void 0) return walkRoute(ctx, modules, route);
4183
- return walkRoute(ctx, modules, route, {
4184
- /**
4185
- * Enters the bookmark the walk starts from, checked the way `restore` checks it.
4186
- *
4187
- * @param bookmark - The bookmark to enter.
4188
- * @returns A promise that resolves once the loop rests at its node.
4189
- */
4190
- restore: (bookmark) => enterBookmark(ctx, modules, bookmark),
4191
- from
4192
- });
4456
+ if (from === void 0 || state.running === void 0) return walkRoute(ctx, modules, route);
4457
+ return walkFrom(ctx, modules, route, from);
4193
4458
  },
4194
4459
  bookmark: () => makeBookmark(ctx, modules),
4195
- restore: (bookmark) => enterBookmark(ctx, modules, bookmark),
4196
- describe: () => graphOf(ctx, modules),
4460
+ restore: (bookmark) => restoreBookmark(ctx, modules, bookmark),
4461
+ describe: () => graphOf(ctx, modules.features),
4197
4462
  state: () => readState(ctx),
4198
4463
  history: () => [...state.journal],
4199
4464
  setMode: (mode) => {
@@ -4521,7 +4786,7 @@ const flowPlugin = /*#__PURE__*/ createPlugin$1("flow", {
4521
4786
  },
4522
4787
  events: (register) => register.map({
4523
4788
  "flow:edge": "An edge was taken and its state committed",
4524
- "flow:rest": "The graph reached a rest node",
4789
+ "flow:rest": "The graph reached a rest node, or entered the node of a bookmark",
4525
4790
  "flow:error": "A node failed and the graph rolled back"
4526
4791
  }),
4527
4792
  createState: createFlowState,
@@ -17236,8 +17501,32 @@ async function untilAborted(ctx, bundle, signal) {
17236
17501
  if (load.failed && !signal.aborted) throw load.error;
17237
17502
  }
17238
17503
  /**
17504
+ * Takes the expected scene and clears the record, so one `expect` mounts one scene.
17505
+ *
17506
+ * @param ctx - Domain context of the plugin.
17507
+ * @returns The scene id that was recorded or expected, or `undefined` when there was none.
17508
+ */
17509
+ function takePending(ctx) {
17510
+ const target = ctx.state.pending;
17511
+ ctx.state.pending = void 0;
17512
+ return target;
17513
+ }
17514
+ /**
17515
+ * Tells whether the node is a transit node of a fast walk: the walk passes through it and builds
17516
+ * no scene there.
17517
+ *
17518
+ * @param node - The node being entered.
17519
+ * @param run - The runner's mode and signal.
17520
+ * @returns True for a node without `rest` entered in fast mode.
17521
+ */
17522
+ function isFastTransit(node, run) {
17523
+ return run.mode === "fast" && !node.rest;
17524
+ }
17525
+ /**
17239
17526
  * Reads the scene the node asks for. In fast mode a transit node only records it: the walk passes
17240
- * through many of them and the picture is built once, at the rest point.
17527
+ * through many of them and the picture is built once, at the rest point. In live mode a transit
17528
+ * node without a scene of its own takes the expected one, so a restore at a popup that waits for
17529
+ * its answer builds the scene under it; one with its own scene leaves the record alone.
17241
17530
  *
17242
17531
  * @param ctx - Domain context of the plugin.
17243
17532
  * @param node - The node being entered.
@@ -17246,32 +17535,33 @@ async function untilAborted(ctx, bundle, signal) {
17246
17535
  */
17247
17536
  function targetOf(ctx, node, run) {
17248
17537
  const state = ctx.state;
17249
- if (run.mode === "fast" && !node.rest) {
17250
- if (node.scene !== void 0) state.pending = node.scene;
17538
+ if (isFastTransit(node, run)) {
17539
+ state.pending = node.scene ?? state.pending;
17251
17540
  return;
17252
17541
  }
17253
- if (!node.rest) return node.scene;
17542
+ if (!node.rest) return node.scene ?? takePending(ctx);
17254
17543
  const target = node.scene ?? state.pending;
17255
17544
  state.pending = void 0;
17256
17545
  if (target === void 0 && state.current === void 0) ctx.log.warn("scenes: a rest node was entered with no scene", { path: node.path });
17257
17546
  return target;
17258
17547
  }
17259
17548
  /**
17260
- * Reads the scene an `over` node asks for. An over node never switches a mounted scene; a rest
17261
- * one takes the expected scene while nothing is mounted, so a restore at a popup builds the scene
17262
- * under it. A scene the over node names itself is only warned about.
17549
+ * Reads the scene an `over` node asks for. An over node never switches a mounted scene; it takes
17550
+ * the expected scene while nothing is mounted, so a restore at a popup builds the scene under it,
17551
+ * whether the popup rests or waits for its answer. In fast mode an over transit node takes
17552
+ * nothing and leaves the record alone: a walk builds no scene on the way. A scene the over node
17553
+ * names itself is only warned about.
17263
17554
  *
17264
17555
  * @param ctx - Domain context of the plugin.
17265
17556
  * @param node - The over node being entered.
17557
+ * @param run - The runner's mode and signal.
17266
17558
  * @returns The expected scene id, or `undefined` when nothing has to happen.
17267
17559
  */
17268
- function overTargetOf(ctx, node) {
17269
- const state = ctx.state;
17560
+ function overTargetOf(ctx, node, run) {
17270
17561
  if (node.scene !== void 0) ctx.log.warn("scenes: an over node cannot name a scene", { path: node.path });
17271
- if (!node.rest || state.current !== void 0) return void 0;
17272
- const target = state.pending;
17273
- state.pending = void 0;
17274
- return target;
17562
+ if (isFastTransit(node, run)) return void 0;
17563
+ if (ctx.state.current !== void 0) return void 0;
17564
+ return takePending(ctx);
17275
17565
  }
17276
17566
  /**
17277
17567
  * Builds the scene in one synchronous block: the layers, then the projections of the old scene
@@ -17317,7 +17607,7 @@ function apply(ctx, scene) {
17317
17607
  * ```
17318
17608
  */
17319
17609
  async function enterScene(ctx, node, run) {
17320
- const target = node.over ? overTargetOf(ctx, node) : targetOf(ctx, node, run);
17610
+ const target = node.over ? overTargetOf(ctx, node, run) : targetOf(ctx, node, run);
17321
17611
  if (target === void 0 || target === ctx.state.current) return;
17322
17612
  const scene = ctx.state.scenes.get(target);
17323
17613
  if (scene === void 0) throw new Error(`[game] Scene "${target}" is not registered.\n List it in the scenes of a feature.`);