@moku-labs/game 0.14.0 → 0.14.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.
- package/dist/app/page.d.mts +1 -1
- package/dist/app/system.d.mts +3 -3
- package/dist/app.d.mts +2 -2
- package/dist/app.mjs +1 -1
- package/dist/assets.d.mts +1 -1
- package/dist/{commands-Cpn2ga7s.d.mts → commands-DWuuqLHu.d.mts} +5 -5
- package/dist/control.d.mts +4 -4
- package/dist/{define-Bwp8CNkW.d.mts → define-HP1Xow8i.d.mts} +2 -2
- package/dist/{headless-IfD6_JMH.d.mts → headless-CBQhbYPZ.d.mts} +1 -1
- package/dist/{index-yUO4Dx2C.d.mts → index-CGynRcOj.d.mts} +6 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/inspect.d.mts +7 -7
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{memory-D1IKoFr1.d.mts → memory-DUNdViKq.d.mts} +1 -1
- package/dist/{runtime-CWY6co6m.d.mts → runtime-CRfdX18W.d.mts} +1 -1
- package/dist/{src-Dgmdzl8R.mjs → src-BD-MZliM.mjs} +354 -62
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +1 -1
- package/dist/{types-BWDESfx2.d.mts → types-BAWV0bNA.d.mts} +105 -11
- package/dist/{types-InZGjvkI.d.mts → types-BVHYu1kz.d.mts} +2 -2
- package/dist/{types-oQoQEj-y.d.mts → types-DO_5UT1J.d.mts} +14 -8
- package/dist/{types-D7MGz4c-.d.mts → types-c50K8lVm.d.mts} +2 -2
- package/dist/{types-B98iRBSG.d.mts → types-v3HEx-i8.d.mts} +4 -4
- package/dist/{types-BArxen_M.d.mts → types-zzul0fH3.d.mts} +1 -1
- package/dist/visual.d.mts +6 -6
- package/llms.txt +6 -2
- package/package.json +1 -1
|
@@ -1327,7 +1327,7 @@ function createFeaturesApi(ctx) {
|
|
|
1327
1327
|
* asError("no audio device").message; // "no audio device"
|
|
1328
1328
|
* ```
|
|
1329
1329
|
*/
|
|
1330
|
-
function asError$
|
|
1330
|
+
function asError$5(error) {
|
|
1331
1331
|
return error instanceof Error ? error : new Error(String(error));
|
|
1332
1332
|
}
|
|
1333
1333
|
/**
|
|
@@ -1339,7 +1339,7 @@ function asError$4(error) {
|
|
|
1339
1339
|
* @param error - What the handler threw or rejected with.
|
|
1340
1340
|
*/
|
|
1341
1341
|
function reportFailure(log, kind, error) {
|
|
1342
|
-
log.error("flow:fx-handler-failed", { kind }, asError$
|
|
1342
|
+
log.error("flow:fx-handler-failed", { kind }, asError$5(error));
|
|
1343
1343
|
}
|
|
1344
1344
|
/**
|
|
1345
1345
|
* Hands one released hint to every `onHint` listener, in registration order. A listener lives
|
|
@@ -1355,7 +1355,7 @@ function notifyHint(state, log, item) {
|
|
|
1355
1355
|
for (const listener of listeners) try {
|
|
1356
1356
|
listener(item);
|
|
1357
1357
|
} catch (error) {
|
|
1358
|
-
log.error("flow:hint-listener-failed", { kind: item.kind }, asError$
|
|
1358
|
+
log.error("flow:hint-listener-failed", { kind: item.kind }, asError$5(error));
|
|
1359
1359
|
}
|
|
1360
1360
|
}
|
|
1361
1361
|
/**
|
|
@@ -1708,9 +1708,10 @@ function deliver(state, answer) {
|
|
|
1708
1708
|
resolve?.(answer);
|
|
1709
1709
|
}
|
|
1710
1710
|
/**
|
|
1711
|
-
* Keeps an answer that arrived at a closed gate
|
|
1712
|
-
* while time is not running: there is no frame that could
|
|
1713
|
-
*
|
|
1711
|
+
* Keeps an answer that arrived at a closed gate, with the frame and the game time of the refusal.
|
|
1712
|
+
* Nothing is held in fast mode or while time is not running: there is no frame that could age it.
|
|
1713
|
+
* Without a hold time (`holdMs` 0 or less) the first answer wins and a second one is dropped. With
|
|
1714
|
+
* one, every refused answer replaces the held one: the last press is the one the player means.
|
|
1714
1715
|
*
|
|
1715
1716
|
* @param ctx - Domain context of the flow plugin.
|
|
1716
1717
|
* @param answer - The answer the closed gate refused.
|
|
@@ -1718,15 +1719,18 @@ function deliver(state, answer) {
|
|
|
1718
1719
|
function hold(ctx, answer) {
|
|
1719
1720
|
if (ctx.state.fx.mode === "fast") return;
|
|
1720
1721
|
if (!ctx.deps.time.isRunning()) return;
|
|
1721
|
-
if (ctx.state.gate.held !== void 0) return;
|
|
1722
|
+
if (ctx.config.holdMs <= 0 && ctx.state.gate.held !== void 0) return;
|
|
1723
|
+
const { frame, elapsed } = ctx.deps.time.snapshot();
|
|
1722
1724
|
ctx.state.gate.held = {
|
|
1723
1725
|
answer,
|
|
1724
|
-
frame
|
|
1726
|
+
frame,
|
|
1727
|
+
at: elapsed
|
|
1725
1728
|
};
|
|
1726
1729
|
}
|
|
1727
1730
|
/**
|
|
1728
1731
|
* Offers the held answer to the gate that has just opened. It is offered once: an answer the new
|
|
1729
|
-
* gate does not accept is dropped, not kept for the next one.
|
|
1732
|
+
* gate does not accept is dropped, not kept for the next one. A gate that opens in fast mode gets
|
|
1733
|
+
* no offer: a fast walk answers its own gates, and a press of the live play before it is dropped.
|
|
1730
1734
|
*
|
|
1731
1735
|
* @param ctx - Domain context of the flow plugin.
|
|
1732
1736
|
*/
|
|
@@ -1734,13 +1738,15 @@ function offerHeld(ctx) {
|
|
|
1734
1738
|
const held = ctx.state.gate.held;
|
|
1735
1739
|
if (held === void 0) return;
|
|
1736
1740
|
ctx.state.gate.held = void 0;
|
|
1741
|
+
if (ctx.state.fx.mode === "fast") return;
|
|
1737
1742
|
if (!accepts(ctx.state.gate, held.answer)) return;
|
|
1738
1743
|
deliver(ctx.state.gate, held.answer);
|
|
1739
1744
|
}
|
|
1740
1745
|
/**
|
|
1741
1746
|
* Creates the gate API: the single entry of player answers. The gate closes before the answer is
|
|
1742
|
-
* handed on; an answer to a closed gate is held for
|
|
1743
|
-
* answer through; `pointer(true)` delays entering
|
|
1747
|
+
* handed on; an answer to a closed gate is held for the next gate, for one frame or for
|
|
1748
|
+
* `config.holdMs` of game time; `narrow` lets one answer through; `pointer(true)` delays entering
|
|
1749
|
+
* an `over` node.
|
|
1744
1750
|
*
|
|
1745
1751
|
* @param ctx - Domain context of the flow plugin.
|
|
1746
1752
|
* @returns The public gate API plus the methods injected into `runner` and `fx`.
|
|
@@ -1778,9 +1784,14 @@ function createGateApi(ctx) {
|
|
|
1778
1784
|
narrow: (allow) => {
|
|
1779
1785
|
ctx.state.gate.narrow = allow;
|
|
1780
1786
|
},
|
|
1781
|
-
clearHeld: (frame) => {
|
|
1782
|
-
|
|
1783
|
-
if (
|
|
1787
|
+
clearHeld: (frame, elapsed) => {
|
|
1788
|
+
const held = ctx.state.gate.held;
|
|
1789
|
+
if (held === void 0) return;
|
|
1790
|
+
if (held.frame === frame) return;
|
|
1791
|
+
if (ctx.config.holdMs > 0 && elapsed - held.at <= ctx.config.holdMs) return;
|
|
1792
|
+
ctx.state.gate.held = void 0;
|
|
1793
|
+
},
|
|
1794
|
+
dropHeld: () => {
|
|
1784
1795
|
ctx.state.gate.held = void 0;
|
|
1785
1796
|
}
|
|
1786
1797
|
};
|
|
@@ -1908,10 +1919,11 @@ function registerSchedule(ctx, fx) {
|
|
|
1908
1919
|
}
|
|
1909
1920
|
/**
|
|
1910
1921
|
* Connects flow to its dependencies in `onInit`: posts `elapsed` from `clock.onElapsed` into the
|
|
1911
|
-
* inbox, flushes effect completions and the gate hold in `time.onFrame("signals")`,
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1922
|
+
* inbox, flushes effect completions and ages the gate hold in `time.onFrame("signals")`, with the
|
|
1923
|
+
* frame and the elapsed game time of that frame, and registers the `schedule` handler. `onInit`
|
|
1924
|
+
* has no access to the plugin's own API, so it builds its own module objects — they are pure
|
|
1925
|
+
* functions over `ctx.state`, which is the one place the modules keep their data, so these
|
|
1926
|
+
* objects and the API's objects are the same gate and the same fx.
|
|
1915
1927
|
*
|
|
1916
1928
|
* @param ctx - Kernel context of the flow plugin.
|
|
1917
1929
|
*/
|
|
@@ -1926,9 +1938,9 @@ function connectFlow(ctx) {
|
|
|
1926
1938
|
payload: input
|
|
1927
1939
|
});
|
|
1928
1940
|
});
|
|
1929
|
-
flowCtx.deps.time.onFrame("signals", ({ frame }) => {
|
|
1941
|
+
flowCtx.deps.time.onFrame("signals", ({ frame, elapsed }) => {
|
|
1930
1942
|
fx.flushSettled();
|
|
1931
|
-
gate.clearHeld(frame);
|
|
1943
|
+
gate.clearHeld(frame, elapsed);
|
|
1932
1944
|
});
|
|
1933
1945
|
registerSchedule(flowCtx, fx);
|
|
1934
1946
|
}
|
|
@@ -2086,6 +2098,25 @@ function nodeProblems(flow) {
|
|
|
2086
2098
|
return problems;
|
|
2087
2099
|
}
|
|
2088
2100
|
/**
|
|
2101
|
+
* Reports a `recover` on a node that does not rest. The recovery makes its node the rest point
|
|
2102
|
+
* of the loop, so only a rest node can carry one.
|
|
2103
|
+
*
|
|
2104
|
+
* @param flow - The flow being validated.
|
|
2105
|
+
* @returns One sentence per problem.
|
|
2106
|
+
* @example
|
|
2107
|
+
* ```ts
|
|
2108
|
+
* recoverProblems(boardFlow); // []: no transit node of the board has a recover
|
|
2109
|
+
* ```
|
|
2110
|
+
*/
|
|
2111
|
+
function recoverProblems(flow) {
|
|
2112
|
+
const problems = [];
|
|
2113
|
+
for (const [name, entry] of Object.entries(flow.nodes)) {
|
|
2114
|
+
if (entry.kind !== "node" || entry.recover === void 0 || entry.rest) continue;
|
|
2115
|
+
problems.push(`[game] Flow "${flow.id}": node "${name}" has recover but is not a rest node.\n Add rest: true, or remove recover.`);
|
|
2116
|
+
}
|
|
2117
|
+
return problems;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2089
2120
|
* Reports a start node that is not in `nodes`, and every node the start cannot reach.
|
|
2090
2121
|
*
|
|
2091
2122
|
* @param flow - The flow being validated.
|
|
@@ -2286,6 +2317,32 @@ function safeNodeProblems(config, features) {
|
|
|
2286
2317
|
return [`[game] The safe node "${safeNode}" is not a checkpoint.\n Mark it checkpoint: true or choose another rest node.`];
|
|
2287
2318
|
}
|
|
2288
2319
|
/**
|
|
2320
|
+
* Warns about a `recover` on a node that is not the safe node. The runner calls `recover` only in
|
|
2321
|
+
* the safe node, after it gave up on a transition, so the hook never runs anywhere else. The safe
|
|
2322
|
+
* node is the configured one, or the start of the main flow. Nothing is warned about while that
|
|
2323
|
+
* path names no node: a wrong `safeNode` is a problem of its own, and behind a start that is a
|
|
2324
|
+
* sub-flow or a slot the node the loop enters is not known before the game runs.
|
|
2325
|
+
*
|
|
2326
|
+
* @param flows - Every collected flow by id.
|
|
2327
|
+
* @param features - Features API, read for a safe node inside a slot contribution.
|
|
2328
|
+
* @param config - Resolved flow config: `mainFlow`, `safeNode`.
|
|
2329
|
+
* @returns One warning per node whose `recover` never runs.
|
|
2330
|
+
*/
|
|
2331
|
+
function recoverWarnings(flows, features, config) {
|
|
2332
|
+
const { mainFlow } = config;
|
|
2333
|
+
if (!mainFlow) return [];
|
|
2334
|
+
const safePath = config.safeNode ?? mainFlow.start;
|
|
2335
|
+
const safe = findNode(mainFlow, safePath, features.contributions);
|
|
2336
|
+
if (safe?.entry.kind !== "node") return [];
|
|
2337
|
+
const warnings = [];
|
|
2338
|
+
for (const flow of flows.values()) for (const [name, entry] of Object.entries(flow.nodes)) {
|
|
2339
|
+
if (entry.kind !== "node" || entry.recover === void 0) continue;
|
|
2340
|
+
if (safe.flow.id === flow.id && safe.name === name) continue;
|
|
2341
|
+
warnings.push(`[game] Flow "${flow.id}": node "${name}" has recover but is not the safe node.\n The engine runs recover only in the safe node "${safePath}". Move recover there or remove it.`);
|
|
2342
|
+
}
|
|
2343
|
+
return warnings;
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2289
2346
|
* Warns when two contributions of one slot have a node with the same name. A bookmark path keeps
|
|
2290
2347
|
* node names only, so `restore` cannot tell the two apart and enters the first one in order.
|
|
2291
2348
|
*
|
|
@@ -2326,9 +2383,11 @@ function sizeWarnings(flow) {
|
|
|
2326
2383
|
* unreachable node, outcome without an edge, missing target, barrier edge that does not reach a
|
|
2327
2384
|
* rest node of the same flow, cycle with no rest node, slot without a name, equal contribution
|
|
2328
2385
|
* `order`, one flow contributed twice to a slot, `inbox` type that is not an outcome, rest node
|
|
2329
|
-
* with neither `run` nor outcomes, a
|
|
2330
|
-
* scene, `safeNode` that is not a checkpoint,
|
|
2331
|
-
*
|
|
2386
|
+
* with neither `run` nor outcomes, a `recover` on a node that is not a rest node, a scene id no
|
|
2387
|
+
* feature registers, an `over` node that names a scene, `safeNode` that is not a checkpoint,
|
|
2388
|
+
* colliding ids. A `recover` on a node that is not the safe node is a warning: it never runs
|
|
2389
|
+
* there. Types are bypassed by JSON data and casts, so this runs even though the edge table is
|
|
2390
|
+
* checked at compile time.
|
|
2332
2391
|
*
|
|
2333
2392
|
* @param flows - Every collected flow by id, the main flow first.
|
|
2334
2393
|
* @param features - Features API: slot contributions and feature names.
|
|
@@ -2339,10 +2398,10 @@ function validateGraph(flows, features, config) {
|
|
|
2339
2398
|
const problems = [];
|
|
2340
2399
|
const warnings = [];
|
|
2341
2400
|
for (const flow of flows.values()) {
|
|
2342
|
-
problems.push(...edgeProblems(flow), ...nodeProblems(flow), ...reachProblems(flow), ...cycleProblems(flow));
|
|
2401
|
+
problems.push(...edgeProblems(flow), ...nodeProblems(flow), ...recoverProblems(flow), ...reachProblems(flow), ...cycleProblems(flow));
|
|
2343
2402
|
warnings.push(...sizeWarnings(flow));
|
|
2344
2403
|
}
|
|
2345
|
-
warnings.push(...slotNameWarnings(flows, features));
|
|
2404
|
+
warnings.push(...slotNameWarnings(flows, features), ...recoverWarnings(flows, features, config));
|
|
2346
2405
|
problems.push(...flowIdProblems(flows), ...featureProblems(flows, features), ...sceneProblems(flows, features), ...safeNodeProblems(config, features));
|
|
2347
2406
|
return {
|
|
2348
2407
|
problems,
|
|
@@ -2882,8 +2941,22 @@ function continueSlot(frames, finished) {
|
|
|
2882
2941
|
//#endregion
|
|
2883
2942
|
//#region src/plugins/flow/runner/edge.ts
|
|
2884
2943
|
/**
|
|
2944
|
+
* Turns whatever a node failed with into an error the log takes.
|
|
2945
|
+
*
|
|
2946
|
+
* @param value - The thrown value or the rejection reason.
|
|
2947
|
+
* @returns The value itself when it is an error, a new error with its text otherwise.
|
|
2948
|
+
* @example
|
|
2949
|
+
* ```ts
|
|
2950
|
+
* asError("no save").message; // "no save"
|
|
2951
|
+
* ```
|
|
2952
|
+
*/
|
|
2953
|
+
function asError$4(value) {
|
|
2954
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2885
2957
|
* Commits the transaction of one node, releases its hints, journals the edge and emits
|
|
2886
|
-
* `flow:edge`. This is the only commit of the engine
|
|
2958
|
+
* `flow:edge`. This is the only commit of the engine: the edge of a node, and the recovery edge
|
|
2959
|
+
* of the safe node, both go through it.
|
|
2887
2960
|
*
|
|
2888
2961
|
* @param ctx - Domain context of the flow plugin.
|
|
2889
2962
|
* @param modules - Injected sibling APIs.
|
|
@@ -2916,7 +2989,11 @@ function commitEdge(ctx, modules, step, next) {
|
|
|
2916
2989
|
}
|
|
2917
2990
|
/**
|
|
2918
2991
|
* Discards everything the failed node did, rolls back and decides between a retry and the safe
|
|
2919
|
-
* node.
|
|
2992
|
+
* node. Every failure is written to the log as one error entry `flow:error`, right before the
|
|
2993
|
+
* event of the same name. When the loop gives up it stands on the safe node, whose `recover` is
|
|
2994
|
+
* then still to run, and an answer the gate still held is dropped; a retry keeps it. A failure
|
|
2995
|
+
* while the loop already recovers in the safe node is fatal: it is no event and no log entry, it
|
|
2996
|
+
* rejects `run()`.
|
|
2920
2997
|
*
|
|
2921
2998
|
* @param ctx - Domain context of the flow plugin.
|
|
2922
2999
|
* @param modules - Injected sibling APIs.
|
|
@@ -2938,17 +3015,25 @@ function handleFailure(ctx, modules, transaction, error, path, safe) {
|
|
|
2938
3015
|
const retry = state.failures <= ctx.config.retries;
|
|
2939
3016
|
const safeTarget = () => safeFrames(ctx, modules.features.contributions);
|
|
2940
3017
|
const target = retry ? state.restFrame ?? safeTarget() : safeTarget();
|
|
3018
|
+
const rolledBackTo = framePath(target);
|
|
3019
|
+
ctx.log.error("flow:error", {
|
|
3020
|
+
path,
|
|
3021
|
+
rolledBackTo,
|
|
3022
|
+
retry
|
|
3023
|
+
}, asError$4(error));
|
|
2941
3024
|
ctx.emit("flow:error", {
|
|
2942
3025
|
path,
|
|
2943
3026
|
error,
|
|
2944
|
-
rolledBackTo
|
|
3027
|
+
rolledBackTo,
|
|
2945
3028
|
retry
|
|
2946
3029
|
});
|
|
2947
3030
|
state.stack = [...target];
|
|
2948
3031
|
state.slotAfter = void 0;
|
|
2949
3032
|
if (!retry) {
|
|
2950
3033
|
safe.inside = true;
|
|
3034
|
+
safe.recover = true;
|
|
2951
3035
|
state.failures = 0;
|
|
3036
|
+
modules.gate.dropHeld();
|
|
2952
3037
|
}
|
|
2953
3038
|
return "continue";
|
|
2954
3039
|
}
|
|
@@ -3057,7 +3142,9 @@ function type() {
|
|
|
3057
3142
|
}
|
|
3058
3143
|
/**
|
|
3059
3144
|
* Defines a node. `out` of the body has one method per declared outcome; `run` is required
|
|
3060
|
-
* unless `rest: true`.
|
|
3145
|
+
* unless `rest: true`. A rest node may carry `recover`: the engine runs it before the node is
|
|
3146
|
+
* entered as the safe node. `Game` is bound by `defineGame<Types>()`; here it is the engine's
|
|
3147
|
+
* `Json`.
|
|
3061
3148
|
*
|
|
3062
3149
|
* @param spec - Input tag, outcomes, flags, inbox and the body.
|
|
3063
3150
|
* @returns The node as plain data.
|
|
@@ -3084,7 +3171,8 @@ function defineNode(spec) {
|
|
|
3084
3171
|
checkpoint: spec.checkpoint === true,
|
|
3085
3172
|
barrier: spec.barrier === true,
|
|
3086
3173
|
inbox: spec.inbox ?? [],
|
|
3087
|
-
...spec.scene === void 0 ? {} : { scene: spec.scene }
|
|
3174
|
+
...spec.scene === void 0 ? {} : { scene: spec.scene },
|
|
3175
|
+
...spec.recover === void 0 ? {} : { recover: spec.recover }
|
|
3088
3176
|
};
|
|
3089
3177
|
return spec.run ? {
|
|
3090
3178
|
...node,
|
|
@@ -3255,10 +3343,11 @@ function refuseEffect(signal) {
|
|
|
3255
3343
|
* Builds the `fx` of a node context: awaited effects by call, cosmetic hints by `emit`. A `guide`
|
|
3256
3344
|
* narrows the gate while it runs; the runner lifts the narrow when the node is left. A body that
|
|
3257
3345
|
* 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.
|
|
3346
|
+
* any more: the gate and the narrow belong to the node the loop entered after it. The `recover`
|
|
3347
|
+
* of a rest node gets the same `fx`, bound to the signal of its recovery.
|
|
3259
3348
|
*
|
|
3260
3349
|
* @param modules - Injected sibling APIs.
|
|
3261
|
-
* @param step - The
|
|
3350
|
+
* @param step - The run in progress: its abort signal, and how it tells a walk that a gate opened.
|
|
3262
3351
|
* @returns The effects gateway of one node run.
|
|
3263
3352
|
*/
|
|
3264
3353
|
function nodeFx(modules, step) {
|
|
@@ -3379,6 +3468,7 @@ function stopped(state) {
|
|
|
3379
3468
|
}
|
|
3380
3469
|
/**
|
|
3381
3470
|
* Resolves as soon as the node is aborted, so a body that ignores its signal never holds the loop.
|
|
3471
|
+
* A recovery races its `recover` against the same promise.
|
|
3382
3472
|
*
|
|
3383
3473
|
* @param signal - The node's abort signal.
|
|
3384
3474
|
* @returns The abort outcome.
|
|
@@ -3583,6 +3673,136 @@ function waitForPointer(ctx) {
|
|
|
3583
3673
|
});
|
|
3584
3674
|
}
|
|
3585
3675
|
//#endregion
|
|
3676
|
+
//#region src/plugins/flow/runner/recover.ts
|
|
3677
|
+
/**
|
|
3678
|
+
* Takes the recovery the loop owes the safe node. It is owed once per give-up, to the first node
|
|
3679
|
+
* entered after it, and a node without `recover` uses it up as well.
|
|
3680
|
+
*
|
|
3681
|
+
* @param safe - How the loop stands after it gave up.
|
|
3682
|
+
* @param node - The node being entered.
|
|
3683
|
+
* @returns True when the node has a `recover` that must run now.
|
|
3684
|
+
*/
|
|
3685
|
+
function takeRecovery(safe, node) {
|
|
3686
|
+
if (!safe.recover) return false;
|
|
3687
|
+
safe.recover = false;
|
|
3688
|
+
return node.recover !== void 0;
|
|
3689
|
+
}
|
|
3690
|
+
/**
|
|
3691
|
+
* Builds the one object a `recover` receives: the node context without `input` and `out`.
|
|
3692
|
+
*
|
|
3693
|
+
* @param modules - Injected sibling APIs.
|
|
3694
|
+
* @param recovery - The recovery in progress.
|
|
3695
|
+
* @returns The recovery context.
|
|
3696
|
+
*/
|
|
3697
|
+
function recoverContext(modules, recovery) {
|
|
3698
|
+
return {
|
|
3699
|
+
player: recovery.transaction.player,
|
|
3700
|
+
session: recovery.transaction.session,
|
|
3701
|
+
rng: recovery.transaction.rng,
|
|
3702
|
+
fx: nodeFx(modules, recovery),
|
|
3703
|
+
signal: recovery.signal,
|
|
3704
|
+
now: recovery.now
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
/**
|
|
3708
|
+
* Calls the `recover` of a node and turns what it does into an outcome. Nothing is thrown out of
|
|
3709
|
+
* here.
|
|
3710
|
+
*
|
|
3711
|
+
* @param modules - Injected sibling APIs.
|
|
3712
|
+
* @param recovery - The recovery in progress.
|
|
3713
|
+
* @returns That it returned, the abort or the failure.
|
|
3714
|
+
*/
|
|
3715
|
+
async function callRecover(modules, recovery) {
|
|
3716
|
+
try {
|
|
3717
|
+
await recovery.node.recover?.(recoverContext(modules, recovery));
|
|
3718
|
+
if (recovery.signal.aborted) return {
|
|
3719
|
+
kind: "aborted",
|
|
3720
|
+
reason: abortReason(recovery.signal)
|
|
3721
|
+
};
|
|
3722
|
+
return { kind: "recovered" };
|
|
3723
|
+
} catch (error) {
|
|
3724
|
+
if (recovery.signal.aborted) return {
|
|
3725
|
+
kind: "aborted",
|
|
3726
|
+
reason: abortReason(recovery.signal)
|
|
3727
|
+
};
|
|
3728
|
+
return {
|
|
3729
|
+
kind: "failed",
|
|
3730
|
+
error
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* Takes the recovery edge: what `recover` wrote is committed as an edge of the safe node onto
|
|
3736
|
+
* itself, with the outcome `"recover"`, and the node becomes the rest point. The journal entry,
|
|
3737
|
+
* `flow:edge`, the rest mark of the store and `flow:rest` are the ones of any edge into a rest
|
|
3738
|
+
* node, so a later rollback returns to the recovered state.
|
|
3739
|
+
*
|
|
3740
|
+
* @param ctx - Domain context of the flow plugin.
|
|
3741
|
+
* @param modules - Injected sibling APIs.
|
|
3742
|
+
* @param recovery - The recovery whose `recover` returned.
|
|
3743
|
+
*/
|
|
3744
|
+
function commitRecovery(ctx, modules, recovery) {
|
|
3745
|
+
commitEdge(ctx, modules, {
|
|
3746
|
+
location: recovery.location,
|
|
3747
|
+
transaction: recovery.transaction,
|
|
3748
|
+
result: {
|
|
3749
|
+
outcome: "recover",
|
|
3750
|
+
payload: null
|
|
3751
|
+
},
|
|
3752
|
+
barrier: false,
|
|
3753
|
+
now: recovery.now,
|
|
3754
|
+
path: recovery.path
|
|
3755
|
+
}, recovery.path);
|
|
3756
|
+
ctx.deps.model.store.markRest();
|
|
3757
|
+
arrive(ctx, {
|
|
3758
|
+
stack: [...ctx.state.runner.stack],
|
|
3759
|
+
next: recovery.path,
|
|
3760
|
+
rest: true,
|
|
3761
|
+
checkpoint: recovery.node.checkpoint
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Runs the `recover` of the safe node as one transaction, before the node is entered: an abort
|
|
3766
|
+
* controller of its own, an open transaction, `recover` raced against the abort, then the
|
|
3767
|
+
* recovery edge. A `recover` that fails is a failure inside the safe node: fatal, `run()` rejects.
|
|
3768
|
+
* An aborted one leaves like an aborted node: `"stop"` ends the loop, `"restore"` lets the next
|
|
3769
|
+
* turn enter the bookmark. `safe.inside` is not touched: it stays true until the safe node's own
|
|
3770
|
+
* edge commits or a restore leaves the node, so a failure of the safe node after its recovery is
|
|
3771
|
+
* still fatal, never a loop.
|
|
3772
|
+
*
|
|
3773
|
+
* @param ctx - Domain context of the flow plugin.
|
|
3774
|
+
* @param modules - Injected sibling APIs.
|
|
3775
|
+
* @param place - The safe node the loop stands on.
|
|
3776
|
+
* @param safe - How the loop stands after it gave up.
|
|
3777
|
+
* @returns The outcome that ends this turn, or `undefined` when the node is entered now.
|
|
3778
|
+
* @throws {Error} When `recover` failed.
|
|
3779
|
+
*/
|
|
3780
|
+
async function recoverNode(ctx, modules, place, safe) {
|
|
3781
|
+
const state = ctx.state.runner;
|
|
3782
|
+
const abort = new AbortController();
|
|
3783
|
+
state.abort = abort;
|
|
3784
|
+
const recovery = {
|
|
3785
|
+
...place,
|
|
3786
|
+
transaction: ctx.deps.model.store.begin(),
|
|
3787
|
+
signal: abort.signal,
|
|
3788
|
+
now: ctx.deps.clock.now(),
|
|
3789
|
+
/**
|
|
3790
|
+
* Tells a walk that the recovery opened the gate for an answer.
|
|
3791
|
+
*/
|
|
3792
|
+
gateOpened: () => {
|
|
3793
|
+
notifyGateOpen(state);
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
const outcome = await Promise.race([callRecover(modules, recovery), abortOutcome(abort.signal)]);
|
|
3797
|
+
modules.gate.narrow(void 0);
|
|
3798
|
+
modules.fx.endGuides();
|
|
3799
|
+
if (outcome.kind === "failed") return handleFailure(ctx, modules, recovery.transaction, outcome.error, place.path, safe);
|
|
3800
|
+
if (outcome.kind === "aborted") return handleAbort(modules, recovery.transaction, outcome.reason);
|
|
3801
|
+
commitRecovery(ctx, modules, recovery);
|
|
3802
|
+
if (stopped(state)) return "stop";
|
|
3803
|
+
if (state.seam?.restoring !== void 0) return "continue";
|
|
3804
|
+
}
|
|
3805
|
+
//#endregion
|
|
3586
3806
|
//#region src/plugins/flow/runner/stop.ts
|
|
3587
3807
|
/**
|
|
3588
3808
|
* Waits `timeoutMs` of real time. The deadline is not counted in frames: a paused clock runs no
|
|
@@ -3629,8 +3849,9 @@ async function settle$1(ctx, running) {
|
|
|
3629
3849
|
}
|
|
3630
3850
|
/**
|
|
3631
3851
|
* Stops the runner: the active node is aborted with reason `"stop"`, the loop is given
|
|
3632
|
-
* `settleTimeoutMs` to settle,
|
|
3633
|
-
* itself discards the open transaction and shuts the
|
|
3852
|
+
* `settleTimeoutMs` to settle, an answer the gate still held is dropped, and a background flush
|
|
3853
|
+
* that is still running is awaited. The loop itself discards the open transaction and shuts the
|
|
3854
|
+
* gate while it leaves the aborted node.
|
|
3634
3855
|
*
|
|
3635
3856
|
* @param ctx - Config and state of the flow plugin.
|
|
3636
3857
|
* @returns A promise that resolves when the graph stands still.
|
|
@@ -3648,6 +3869,7 @@ async function stopRunner(ctx) {
|
|
|
3648
3869
|
}
|
|
3649
3870
|
const settled = running === void 0 || await settle$1(ctx, running);
|
|
3650
3871
|
state.running = void 0;
|
|
3872
|
+
ctx.state.gate.held = void 0;
|
|
3651
3873
|
if (settled) state.abort = void 0;
|
|
3652
3874
|
const flushing = state.flushing;
|
|
3653
3875
|
if (flushing !== void 0) {
|
|
@@ -3658,8 +3880,8 @@ async function stopRunner(ctx) {
|
|
|
3658
3880
|
//#endregion
|
|
3659
3881
|
//#region src/plugins/flow/runner/loop.ts
|
|
3660
3882
|
/**
|
|
3661
|
-
* Runs one node: wait for the pointer,
|
|
3662
|
-
* outcome, then commit on the edge or recover.
|
|
3883
|
+
* Runs one node: wait for the pointer, the recovery the loop owes the safe node, `onEnter` stages,
|
|
3884
|
+
* open the transaction, produce the outcome, then commit on the edge or recover.
|
|
3663
3885
|
*
|
|
3664
3886
|
* @param ctx - Domain context of the flow plugin.
|
|
3665
3887
|
* @param modules - Injected sibling APIs.
|
|
@@ -3674,6 +3896,14 @@ async function runNode(ctx, modules, location, node, safe) {
|
|
|
3674
3896
|
if (node.over) await waitForPointer(ctx);
|
|
3675
3897
|
if (stopped(state)) return "stop";
|
|
3676
3898
|
if (state.seam?.restoring !== void 0) return "continue";
|
|
3899
|
+
if (takeRecovery(safe, node)) {
|
|
3900
|
+
const ended = await recoverNode(ctx, modules, {
|
|
3901
|
+
location,
|
|
3902
|
+
node,
|
|
3903
|
+
path
|
|
3904
|
+
}, safe);
|
|
3905
|
+
if (ended !== void 0) return ended;
|
|
3906
|
+
}
|
|
3677
3907
|
const abort = new AbortController();
|
|
3678
3908
|
state.abort = abort;
|
|
3679
3909
|
const now = ctx.deps.clock.now();
|
|
@@ -3784,18 +4014,25 @@ function enterSlot(ctx, modules, location, slot, safe) {
|
|
|
3784
4014
|
* node too. A restore leaves the whole document waiting for the provider, and a rollback of the
|
|
3785
4015
|
* node would drop it unsent; and the frames of the transit node are what a failure of it must
|
|
3786
4016
|
* 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.
|
|
4017
|
+
* every node entered here; only a rest node is announced as a checkpoint. A restore wins over
|
|
4018
|
+
* what belonged to the old position: a recovery the loop still owed the safe node does not run on
|
|
4019
|
+
* the restored state, the loop is no longer inside the safe node, so a failure of the restored
|
|
4020
|
+
* node is retried and not fatal, and an answer the gate still held is dropped.
|
|
3788
4021
|
*
|
|
3789
4022
|
* @param ctx - Domain context of the flow plugin.
|
|
3790
4023
|
* @param modules - Injected sibling APIs; the features API resolves a path through a slot.
|
|
3791
4024
|
* @param bookmark - The bookmark to enter.
|
|
4025
|
+
* @param safe - How the loop stands after it gave up. Both of its flags are cleared.
|
|
3792
4026
|
* @throws {Error} When the graph has no node at the bookmark's path.
|
|
3793
4027
|
*/
|
|
3794
|
-
function enterBookmark(ctx, modules, bookmark) {
|
|
4028
|
+
function enterBookmark(ctx, modules, bookmark, safe) {
|
|
3795
4029
|
const state = ctx.state.runner;
|
|
3796
4030
|
const location = findNode(requireMainFlow(ctx), bookmark.path, modules.features.contributions);
|
|
3797
4031
|
loopSeam(state).restoring = void 0;
|
|
3798
4032
|
if (location === void 0) throw new Error(`[game] The bookmark names no node "${bookmark.path}".\n Bookmark a node of the running graph.`);
|
|
4033
|
+
safe.inside = false;
|
|
4034
|
+
safe.recover = false;
|
|
4035
|
+
modules.gate.dropHeld();
|
|
3799
4036
|
ctx.deps.model.store.restore({
|
|
3800
4037
|
player: bookmark.player,
|
|
3801
4038
|
session: bookmark.session,
|
|
@@ -3826,7 +4063,7 @@ async function advance(ctx, modules, safe) {
|
|
|
3826
4063
|
if (stopped(state)) return "stop";
|
|
3827
4064
|
const restoring = state.seam?.restoring;
|
|
3828
4065
|
if (restoring !== void 0) {
|
|
3829
|
-
enterBookmark(ctx, modules, restoring);
|
|
4066
|
+
enterBookmark(ctx, modules, restoring, safe);
|
|
3830
4067
|
return "continue";
|
|
3831
4068
|
}
|
|
3832
4069
|
const location = locateFrames(ctx, state.stack);
|
|
@@ -3838,7 +4075,8 @@ async function advance(ctx, modules, safe) {
|
|
|
3838
4075
|
/**
|
|
3839
4076
|
* Runs the graph until aborted: enter node, `onEnter` stages, begin a transaction, await the
|
|
3840
4077
|
* body, commit on the edge, release hints, journal, emit `flow:edge`, follow the edge. An error
|
|
3841
|
-
* discards, rolls back and retries, then enters `safeNode
|
|
4078
|
+
* discards, rolls back, is logged and retries, then enters `safeNode`, whose `recover` runs first
|
|
4079
|
+
* as one committed edge; a failure inside `safeNode` rejects.
|
|
3842
4080
|
* Abort reasons: `"stop"` ends the loop, `"inbox"` turns the event into the outcome, `"restore"`
|
|
3843
4081
|
* enters the bookmark's frame.
|
|
3844
4082
|
*
|
|
@@ -3862,7 +4100,10 @@ async function runLoop(ctx, modules) {
|
|
|
3862
4100
|
state.restFrame = [...state.stack];
|
|
3863
4101
|
state.slotAfter = void 0;
|
|
3864
4102
|
state.failures = 0;
|
|
3865
|
-
const safe = {
|
|
4103
|
+
const safe = {
|
|
4104
|
+
inside: false,
|
|
4105
|
+
recover: false
|
|
4106
|
+
};
|
|
3866
4107
|
const loop = { running: true };
|
|
3867
4108
|
while (loop.running) loop.running = await advance(ctx, modules, safe) === "continue";
|
|
3868
4109
|
}
|
|
@@ -4518,8 +4759,8 @@ function createModules$3(ctx) {
|
|
|
4518
4759
|
};
|
|
4519
4760
|
}
|
|
4520
4761
|
/**
|
|
4521
|
-
* Reduces the gate to what a game may call. `open`, `close`, `narrow` and `
|
|
4522
|
-
* the runner and to
|
|
4762
|
+
* Reduces the gate to what a game may call. `open`, `close`, `narrow`, `clearHeld` and `dropHeld`
|
|
4763
|
+
* belong to the runner, to `fx` and to the plugin root.
|
|
4523
4764
|
*
|
|
4524
4765
|
* @param gate - The full gate module.
|
|
4525
4766
|
* @returns The public gate API.
|
|
@@ -4809,7 +5050,8 @@ const flowPlugin = /*#__PURE__*/ createPlugin$1("flow", {
|
|
|
4809
5050
|
safeNode: void 0,
|
|
4810
5051
|
retries: 1,
|
|
4811
5052
|
settleTimeoutMs: 2e3,
|
|
4812
|
-
journalLimit: 500
|
|
5053
|
+
journalLimit: 500,
|
|
5054
|
+
holdMs: 0
|
|
4813
5055
|
},
|
|
4814
5056
|
events: (register) => register.map({
|
|
4815
5057
|
"flow:edge": "An edge was taken and its state committed",
|
|
@@ -18210,8 +18452,9 @@ async function bufferOf(ctx, key) {
|
|
|
18210
18452
|
/**
|
|
18211
18453
|
* Plays one sound: the handler of the kind `"sfx"`. Every play is its own source, so the same key
|
|
18212
18454
|
* twice in one frame is heard twice, and the promise resolves when the sound STARTED. The handler
|
|
18213
|
-
* ignores its signal: a sound that began plays to its end. While
|
|
18214
|
-
* pending the sound waits in `pendingSfx`, one per key
|
|
18455
|
+
* ignores its signal: a sound that began plays to its end. While a gesture's `resume()` is
|
|
18456
|
+
* pending the sound waits in `pendingSfx`, one per key. A locked context no gesture is resuming
|
|
18457
|
+
* drops it: the start attempt of the unlock queues nothing.
|
|
18215
18458
|
*
|
|
18216
18459
|
* @param ctx - Domain context of the plugin.
|
|
18217
18460
|
* @param descriptor - The `sfx` descriptor `anim` built.
|
|
@@ -18411,7 +18654,7 @@ function refused(ctx, key) {
|
|
|
18411
18654
|
/**
|
|
18412
18655
|
* Switches the music: the handler of the kind `"music"` and what `scenes:changed` calls. The same
|
|
18413
18656
|
* key does nothing, a new key cross-fades, `null` fades out. While the context is locked the key
|
|
18414
|
-
* is only remembered; the
|
|
18657
|
+
* is only remembered; the unlock starts it. A key that is still on its way counts as the
|
|
18415
18658
|
* same key, and every other request replaces it: the replaced switch starts nothing when its
|
|
18416
18659
|
* buffer or its element is ready, so one tap that unlocks the context and enters a scene with the
|
|
18417
18660
|
* same track starts it once. `config.music` picks the decode or the stream path.
|
|
@@ -18569,17 +18812,22 @@ function resetSession(state) {
|
|
|
18569
18812
|
//#endregion
|
|
18570
18813
|
//#region src/plugins/audio/unlock.ts
|
|
18571
18814
|
/**
|
|
18572
|
-
* @file audio plugin — the unlock: the only file that touches `window`. A browser starts
|
|
18573
|
-
*
|
|
18574
|
-
*
|
|
18575
|
-
*
|
|
18815
|
+
* @file audio plugin — the unlock: the only file that touches `window`. A browser starts an audio
|
|
18816
|
+
* context suspended and lets it run on a user gesture, so two listeners wait for the first one.
|
|
18817
|
+
* They sit on `window`, not on the canvas: the gesture may as well be a button of the game's
|
|
18818
|
+
* loading page, which `input` never sees. A shell that asks for no gesture, such as a native
|
|
18819
|
+
* webview with the requirement switched off, lets the context run at once. So the plugin also
|
|
18820
|
+
* asks once at start, and unlocks with no touch where the answer is yes.
|
|
18576
18821
|
*/
|
|
18577
18822
|
/** The two gestures that unlock a context. The first of them to arrive wins. */
|
|
18578
18823
|
const UNLOCK_EVENTS = ["pointerdown", "touchend"];
|
|
18579
18824
|
/**
|
|
18580
|
-
* Reacts to a resume that went through
|
|
18581
|
-
* sounds fired during the resume and the track a scene
|
|
18582
|
-
* context that still does not run drops the sounds:
|
|
18825
|
+
* Reacts to a resume that went through, of a gesture or of the start attempt. The context is
|
|
18826
|
+
* unlocked only when it really runs; the sounds fired during the resume and the track a scene
|
|
18827
|
+
* declared while it was locked start here. A context that still does not run drops the sounds:
|
|
18828
|
+
* the next gesture is a new moment. A gesture and the start attempt that settle together both
|
|
18829
|
+
* come here, and the music still starts once: a track that is on its way or already plays is not
|
|
18830
|
+
* started again. A later gesture on an unlocked context tries a remembered track again.
|
|
18583
18831
|
*
|
|
18584
18832
|
* @param ctx - Domain context of the plugin.
|
|
18585
18833
|
*/
|
|
@@ -18632,8 +18880,50 @@ function resumeContext(ctx) {
|
|
|
18632
18880
|
});
|
|
18633
18881
|
}
|
|
18634
18882
|
/**
|
|
18635
|
-
*
|
|
18636
|
-
* the context
|
|
18883
|
+
* Reacts to the start attempt that fulfilled. Nothing happens when the app stopped meanwhile, when
|
|
18884
|
+
* a gesture already unlocked the context, or when the context still does not run: the listeners
|
|
18885
|
+
* are there for that. Otherwise the listeners go and the context is unlocked the way a gesture
|
|
18886
|
+
* unlocks it.
|
|
18887
|
+
*
|
|
18888
|
+
* @param ctx - Domain context of the plugin.
|
|
18889
|
+
* @param context - The context the attempt was made on.
|
|
18890
|
+
*/
|
|
18891
|
+
function onStartResumed(ctx, context) {
|
|
18892
|
+
const state = ctx.state;
|
|
18893
|
+
if (state.context !== context || state.unlocked || context.state !== "running") return;
|
|
18894
|
+
removeUnlock(state);
|
|
18895
|
+
onResumed(ctx);
|
|
18896
|
+
}
|
|
18897
|
+
/**
|
|
18898
|
+
* Leaves a refused start attempt alone. Nobody asked for sound yet, so there is nothing to report,
|
|
18899
|
+
* and the listeners are installed: the first gesture asks again.
|
|
18900
|
+
*/
|
|
18901
|
+
function ignoreRefusal() {}
|
|
18902
|
+
/**
|
|
18903
|
+
* Unlocks the context in `onStart` where the shell lets audio play, and waits for a gesture where
|
|
18904
|
+
* it does not. A context that already runs is unlocked at once, with no listener. Any other gets
|
|
18905
|
+
* the two listeners and one `resume()`. `resuming` is not set for it: a browser that wants a
|
|
18906
|
+
* gesture leaves that promise pending, and a sound must not queue behind a promise that may never
|
|
18907
|
+
* settle. The attempt needs no `window`, so it also serves a context given through
|
|
18908
|
+
* `config.context` in a runtime without a DOM.
|
|
18909
|
+
*
|
|
18910
|
+
* @param ctx - Domain context of the plugin.
|
|
18911
|
+
*/
|
|
18912
|
+
function startUnlock(ctx) {
|
|
18913
|
+
const context = ctx.state.context;
|
|
18914
|
+
if (context === void 0) return;
|
|
18915
|
+
if (context.state === "running") {
|
|
18916
|
+
onResumed(ctx);
|
|
18917
|
+
return;
|
|
18918
|
+
}
|
|
18919
|
+
installUnlock(ctx);
|
|
18920
|
+
context.resume().then(() => {
|
|
18921
|
+
onStartResumed(ctx, context);
|
|
18922
|
+
}, ignoreRefusal);
|
|
18923
|
+
}
|
|
18924
|
+
/**
|
|
18925
|
+
* Installs the two unlock listeners on `window`. Without a DOM there is nothing to listen on:
|
|
18926
|
+
* the context stays locked unless the start attempt of `startUnlock` runs it.
|
|
18637
18927
|
*
|
|
18638
18928
|
* @param ctx - Domain context of the plugin.
|
|
18639
18929
|
*/
|
|
@@ -18720,9 +19010,10 @@ function createContext(config) {
|
|
|
18720
19010
|
}
|
|
18721
19011
|
/**
|
|
18722
19012
|
* Starts the plugin in `onStart`: the audio session first, before any sound exists, then both
|
|
18723
|
-
* effect handlers, then the context with its graph and its unlock. The
|
|
18724
|
-
*
|
|
18725
|
-
*
|
|
19013
|
+
* effect handlers, then the context with its graph and its unlock. The unlock asks the context to
|
|
19014
|
+
* run at once: a shell that lets audio play is unlocked with no touch, and a browser that wants a
|
|
19015
|
+
* gesture gets the two listeners. The handlers are registered even headless, so a node that
|
|
19016
|
+
* awaits a sound resolves in plain Bun exactly as it does in a browser.
|
|
18726
19017
|
*
|
|
18727
19018
|
* @param ctx - Kernel context of the audio plugin.
|
|
18728
19019
|
*/
|
|
@@ -18737,7 +19028,7 @@ function startAudio(ctx) {
|
|
|
18737
19028
|
state.context = context;
|
|
18738
19029
|
state.buses = buildGraph(context, state.buses);
|
|
18739
19030
|
applyAllGains(state);
|
|
18740
|
-
|
|
19031
|
+
startUnlock(audio);
|
|
18741
19032
|
}
|
|
18742
19033
|
/**
|
|
18743
19034
|
* Frees everything `onStart` opened: the two handler registrations, the two window listeners, the
|
|
@@ -24501,8 +24792,8 @@ function leave(ctx) {
|
|
|
24501
24792
|
* Runs the Back chain once. `back()` and the provider's `onBack` both go through here, so a test
|
|
24502
24793
|
* and a real press end the same way. Escape is what ui answers with the `escape` button of its
|
|
24503
24794
|
* top root; it also ends the editing of a text field. While the graph moves between nodes the gate
|
|
24504
|
-
* is closed: it holds the answer for one frame, so the press
|
|
24505
|
-
* app in the middle of a step.
|
|
24795
|
+
* is closed: it holds the answer for the next gate (one frame, or `flow.holdMs`), so the press
|
|
24796
|
+
* counts as taken and never leaves the app in the middle of a step.
|
|
24506
24797
|
*
|
|
24507
24798
|
* @param ctx - Kernel context of the platform plugin.
|
|
24508
24799
|
* @returns What took the press, `"none"` without a provider.
|
|
@@ -35424,6 +35715,7 @@ function defineMotion(spec) {
|
|
|
35424
35715
|
* | flow | retries | 1 |
|
|
35425
35716
|
* | flow | settleTimeoutMs | 2000 |
|
|
35426
35717
|
* | flow | journalLimit | 500 |
|
|
35718
|
+
* | flow | holdMs | 0, an answer to a closed gate lives one frame |
|
|
35427
35719
|
* | world | settleMs | 350 |
|
|
35428
35720
|
* | world | reconciledEvent | false |
|
|
35429
35721
|
* | renderer | mount | undefined, inert without a mount |
|