@moku-labs/game 0.14.1 → 0.14.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.
- 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-BMpk-pW7.d.mts} +5 -5
- package/dist/control.d.mts +4 -4
- package/dist/{define-Bwp8CNkW.d.mts → define-TUpLvorL.d.mts} +2 -2
- package/dist/{headless-IfD6_JMH.d.mts → headless-TowWxARz.d.mts} +1 -1
- package/dist/{index-yUO4Dx2C.d.mts → index-qc-2WJnj.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-D6S7BUAo.d.mts} +1 -1
- package/dist/{runtime-CWY6co6m.d.mts → runtime-BYbkAbM9.d.mts} +1 -1
- package/dist/{src-Dgmdzl8R.mjs → src-DiPISDVO.mjs} +511 -84
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +1 -1
- package/dist/{types-B98iRBSG.d.mts → types--IldPLbm.d.mts} +4 -4
- package/dist/{types-BWDESfx2.d.mts → types-CUNcMkD1.d.mts} +131 -13
- package/dist/{types-InZGjvkI.d.mts → types-CsejHij6.d.mts} +2 -2
- package/dist/{types-oQoQEj-y.d.mts → types-DzbRzti4.d.mts} +14 -8
- package/dist/{types-D7MGz4c-.d.mts → types-NgMDwIrO.d.mts} +2 -2
- package/dist/{types-BArxen_M.d.mts → types-p12uOu00.d.mts} +3 -2
- package/dist/visual.d.mts +6 -6
- package/llms.txt +7 -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",
|
|
@@ -6278,6 +6520,7 @@ function clearWorld(state) {
|
|
|
6278
6520
|
state.ecs.offFrame.length = 0;
|
|
6279
6521
|
for (const off of state.projection.offHints) off();
|
|
6280
6522
|
state.projection.offHints.length = 0;
|
|
6523
|
+
state.projection.converged.length = 0;
|
|
6281
6524
|
state.projection.specs.clear();
|
|
6282
6525
|
state.projection.mounted.clear();
|
|
6283
6526
|
state.projection.byEntity.clear();
|
|
@@ -7388,13 +7631,63 @@ function keyedItems(pctx, spec, snapshot) {
|
|
|
7388
7631
|
return next;
|
|
7389
7632
|
}
|
|
7390
7633
|
/**
|
|
7634
|
+
* The last step of a reconcile a restore or a load caused: every live view is written to its rest
|
|
7635
|
+
* pose, whether its item changed or not. An aborted node finishes its animation, which leaves a
|
|
7636
|
+
* view on the end pose, and a view whose item is the same object as before is never touched by
|
|
7637
|
+
* the entries. It runs after the flush: every handle is finished and every track of the entity is
|
|
7638
|
+
* cancelled by then, also an additive one, which writes its base back when it ends. So nothing
|
|
7639
|
+
* writes after it. Muted fields are left out, and `view` does not run again.
|
|
7640
|
+
*
|
|
7641
|
+
* @param pctx - Domain context of the projection module.
|
|
7642
|
+
* @param mounted - The mounted projection, already flushed.
|
|
7643
|
+
*/
|
|
7644
|
+
function convergeMounted(pctx, mounted) {
|
|
7645
|
+
for (const view of mounted.live.values()) writeRestNow(pctx, view, [...view.rest.keys()]);
|
|
7646
|
+
}
|
|
7647
|
+
/**
|
|
7648
|
+
* The same last step for the elements another plugin owns, which no reconcile of a projection
|
|
7649
|
+
* meets: each is written to the rest pose its owner recorded with `setRest`. Every track on the
|
|
7650
|
+
* entity is ended first, so nothing writes after it. Only a component the entity still carries
|
|
7651
|
+
* is written and muted fields are left out. An element that is leaving has its tracks ended and
|
|
7652
|
+
* keeps its place, and the recorded pose of an entity that is gone is skipped. So is one recorded
|
|
7653
|
+
* for a projection view: its own rest pose wins, as in `restOf`, and it is already home.
|
|
7654
|
+
*
|
|
7655
|
+
* @param pctx - Domain context of the projection module.
|
|
7656
|
+
*/
|
|
7657
|
+
function convergeElements(pctx) {
|
|
7658
|
+
const { ecs, components } = pctx.deps;
|
|
7659
|
+
const state = pctx.ctx.state.projection;
|
|
7660
|
+
for (const [entity, rest] of state.rests) {
|
|
7661
|
+
if (ecs.ownerOf(entity) === void 0 || state.byEntity.has(entity)) continue;
|
|
7662
|
+
cancelTracks(pctx, entity);
|
|
7663
|
+
if (ecs.has(entity, components.Exiting)) continue;
|
|
7664
|
+
const carried = [];
|
|
7665
|
+
for (const [name, value] of rest) if (storedValue(pctx, entity, value.type) !== void 0) carried.push(name);
|
|
7666
|
+
writeRestNow(pctx, createViewRecord(entity, "", "", void 0, rest), carried);
|
|
7667
|
+
}
|
|
7668
|
+
}
|
|
7669
|
+
/**
|
|
7670
|
+
* Tells every `onConverged` listener that the picture is home, so an owner puts back what only
|
|
7671
|
+
* it knows: `ui` builds its roots again. A listener that throws is logged and the others still
|
|
7672
|
+
* run.
|
|
7673
|
+
*
|
|
7674
|
+
* @param pctx - Domain context of the projection module.
|
|
7675
|
+
*/
|
|
7676
|
+
function tellConverged(pctx) {
|
|
7677
|
+
for (const listener of [...pctx.ctx.state.projection.converged]) try {
|
|
7678
|
+
listener();
|
|
7679
|
+
} catch (error) {
|
|
7680
|
+
pctx.ctx.log.error("world:converged-listener-failed", {}, asError$2(error));
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
/**
|
|
7391
7684
|
* Reconciles one mounted projection against the snapshot.
|
|
7392
7685
|
*
|
|
7393
7686
|
* @param pctx - Domain context of the projection module.
|
|
7394
7687
|
* @param spec - The projection spec.
|
|
7395
7688
|
* @param mounted - The mounted projection.
|
|
7396
7689
|
* @param snapshot - The model snapshot of this reconcile.
|
|
7397
|
-
* @param options - Mode and
|
|
7690
|
+
* @param options - Mode, force flag and converge flag of this reconcile.
|
|
7398
7691
|
* @param counts - The counters of this reconcile.
|
|
7399
7692
|
* @param used - The hints that reached an entry.
|
|
7400
7693
|
*/
|
|
@@ -7423,13 +7716,16 @@ function reconcileOne(pctx, spec, mounted, snapshot, options, counts, used) {
|
|
|
7423
7716
|
}
|
|
7424
7717
|
mounted.items = next;
|
|
7425
7718
|
if (options.direct) flushMounted(pctx, mounted);
|
|
7719
|
+
if (options.converge) convergeMounted(pctx, mounted);
|
|
7426
7720
|
counts.queued += mounted.queue.length;
|
|
7427
7721
|
}
|
|
7428
7722
|
/**
|
|
7429
|
-
* Runs one reconcile over every mounted projection, or over the named ones only.
|
|
7723
|
+
* Runs one reconcile over every mounted projection, or over the named ones only. A converging
|
|
7724
|
+
* one then brings the elements other plugins own home and tells their owners.
|
|
7430
7725
|
*
|
|
7431
7726
|
* @param pctx - Domain context of the projection module.
|
|
7432
|
-
* @param options - Which projections, play or direct,
|
|
7727
|
+
* @param options - Which projections, play or direct, whether `view` is forced, and whether
|
|
7728
|
+
* every live view and every element is brought to its rest pose at the end.
|
|
7433
7729
|
*/
|
|
7434
7730
|
function reconcile(pctx, options) {
|
|
7435
7731
|
const state = pctx.ctx.state.projection;
|
|
@@ -7443,6 +7739,10 @@ function reconcile(pctx, options) {
|
|
|
7443
7739
|
counts.projections += 1;
|
|
7444
7740
|
reconcileOne(pctx, spec, mounted, snapshot, options, counts, used);
|
|
7445
7741
|
}
|
|
7742
|
+
if (options.converge) {
|
|
7743
|
+
convergeElements(pctx);
|
|
7744
|
+
tellConverged(pctx);
|
|
7745
|
+
}
|
|
7446
7746
|
counts.hintsRouted = used.size;
|
|
7447
7747
|
counts.hintsDropped = state.hints.length - used.size;
|
|
7448
7748
|
if (pctx.ctx.config.reconciledEvent) pctx.ctx.emit("world:reconciled", {
|
|
@@ -7479,6 +7779,22 @@ function isDirectCause(cause) {
|
|
|
7479
7779
|
return cause === "load" || cause === "restore" || cause === "mount" || cause === "rerun";
|
|
7480
7780
|
}
|
|
7481
7781
|
/**
|
|
7782
|
+
* Tells whether a cause replaces the state, so every live view has to stand on its rest pose
|
|
7783
|
+
* when the reconcile ends, also a view whose item is the same object as before, and every
|
|
7784
|
+
* element on the rest pose its owner recorded.
|
|
7785
|
+
*
|
|
7786
|
+
* @param cause - Why the reconcile runs.
|
|
7787
|
+
* @returns True for a load and a restore.
|
|
7788
|
+
* @example
|
|
7789
|
+
* ```ts
|
|
7790
|
+
* isConvergeCause("restore"); // true
|
|
7791
|
+
* isConvergeCause("rerun"); // false
|
|
7792
|
+
* ```
|
|
7793
|
+
*/
|
|
7794
|
+
function isConvergeCause(cause) {
|
|
7795
|
+
return cause === "load" || cause === "restore";
|
|
7796
|
+
}
|
|
7797
|
+
/**
|
|
7482
7798
|
* Creates the empty view tables of a freshly mounted projection. These live in their own
|
|
7483
7799
|
* non-exported functions because lint rule L5 refuses a collection built inside an exported
|
|
7484
7800
|
* declaration.
|
|
@@ -7609,16 +7925,19 @@ function createProjectionApi(ctx, deps) {
|
|
|
7609
7925
|
};
|
|
7610
7926
|
const state = ctx.state.projection;
|
|
7611
7927
|
/**
|
|
7612
|
-
* Reconciles what is dirty. The cause table decides play or direct
|
|
7928
|
+
* Reconciles what is dirty. The cause table decides play or direct, and whether every live view
|
|
7929
|
+
* and every element with a recorded rest pose is brought to it: a restore and a load do that.
|
|
7613
7930
|
*/
|
|
7614
7931
|
const reconcileIfDirty = () => {
|
|
7615
7932
|
const dirty = state.dirty;
|
|
7616
7933
|
state.dirty = void 0;
|
|
7617
7934
|
if (dirty !== void 0 && (dirty.roots.has("player") || dirty.roots.has("session"))) {
|
|
7618
7935
|
const direct = dirty.force || dirty.causes.some((cause) => isDirectCause(cause)) || deps.ecs.mode() !== "live";
|
|
7936
|
+
const converge = dirty.causes.some((cause) => isConvergeCause(cause));
|
|
7619
7937
|
reconcile(pctx, {
|
|
7620
7938
|
direct,
|
|
7621
|
-
force: dirty.force
|
|
7939
|
+
force: dirty.force,
|
|
7940
|
+
converge
|
|
7622
7941
|
});
|
|
7623
7942
|
}
|
|
7624
7943
|
state.hints.length = 0;
|
|
@@ -7678,7 +7997,8 @@ function createProjectionApi(ctx, deps) {
|
|
|
7678
7997
|
if (fresh.length > 0) reconcile(pctx, {
|
|
7679
7998
|
names: fresh.map((spec) => spec.name),
|
|
7680
7999
|
direct: true,
|
|
7681
|
-
force: true
|
|
8000
|
+
force: true,
|
|
8001
|
+
converge: false
|
|
7682
8002
|
});
|
|
7683
8003
|
},
|
|
7684
8004
|
unmount: (names) => {
|
|
@@ -7792,6 +8112,13 @@ function createProjectionApi(ctx, deps) {
|
|
|
7792
8112
|
state.rests.delete(entity);
|
|
7793
8113
|
};
|
|
7794
8114
|
},
|
|
8115
|
+
onConverged: (listener) => {
|
|
8116
|
+
state.converged.push(listener);
|
|
8117
|
+
return () => {
|
|
8118
|
+
const at = state.converged.indexOf(listener);
|
|
8119
|
+
if (at !== -1) state.converged.splice(at, 1);
|
|
8120
|
+
};
|
|
8121
|
+
},
|
|
7795
8122
|
rerunAll: forceRerun,
|
|
7796
8123
|
markDirty: (roots, cause) => {
|
|
7797
8124
|
const dirty = state.dirty ?? emptyDirty();
|
|
@@ -7955,6 +8282,7 @@ function exposeProjection(projection) {
|
|
|
7955
8282
|
setRest: projection.setRest,
|
|
7956
8283
|
restOf: projection.restOf,
|
|
7957
8284
|
registerKey: projection.registerKey,
|
|
8285
|
+
onConverged: projection.onConverged,
|
|
7958
8286
|
rerunAll: projection.rerunAll
|
|
7959
8287
|
};
|
|
7960
8288
|
}
|
|
@@ -8081,6 +8409,7 @@ function createProjectionState() {
|
|
|
8081
8409
|
keys: emptyMap$7(),
|
|
8082
8410
|
keysByEntity: emptyMap$7(),
|
|
8083
8411
|
mutes: emptyMap$7(),
|
|
8412
|
+
converged: [],
|
|
8084
8413
|
offHints: []
|
|
8085
8414
|
};
|
|
8086
8415
|
}
|
|
@@ -18210,8 +18539,9 @@ async function bufferOf(ctx, key) {
|
|
|
18210
18539
|
/**
|
|
18211
18540
|
* Plays one sound: the handler of the kind `"sfx"`. Every play is its own source, so the same key
|
|
18212
18541
|
* 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
|
|
18542
|
+
* ignores its signal: a sound that began plays to its end. While a gesture's `resume()` is
|
|
18543
|
+
* pending the sound waits in `pendingSfx`, one per key. A locked context no gesture is resuming
|
|
18544
|
+
* drops it: the start attempt of the unlock queues nothing.
|
|
18215
18545
|
*
|
|
18216
18546
|
* @param ctx - Domain context of the plugin.
|
|
18217
18547
|
* @param descriptor - The `sfx` descriptor `anim` built.
|
|
@@ -18411,7 +18741,7 @@ function refused(ctx, key) {
|
|
|
18411
18741
|
/**
|
|
18412
18742
|
* Switches the music: the handler of the kind `"music"` and what `scenes:changed` calls. The same
|
|
18413
18743
|
* key does nothing, a new key cross-fades, `null` fades out. While the context is locked the key
|
|
18414
|
-
* is only remembered; the
|
|
18744
|
+
* is only remembered; the unlock starts it. A key that is still on its way counts as the
|
|
18415
18745
|
* same key, and every other request replaces it: the replaced switch starts nothing when its
|
|
18416
18746
|
* buffer or its element is ready, so one tap that unlocks the context and enters a scene with the
|
|
18417
18747
|
* same track starts it once. `config.music` picks the decode or the stream path.
|
|
@@ -18569,17 +18899,22 @@ function resetSession(state) {
|
|
|
18569
18899
|
//#endregion
|
|
18570
18900
|
//#region src/plugins/audio/unlock.ts
|
|
18571
18901
|
/**
|
|
18572
|
-
* @file audio plugin — the unlock: the only file that touches `window`. A browser starts
|
|
18573
|
-
*
|
|
18574
|
-
*
|
|
18575
|
-
*
|
|
18902
|
+
* @file audio plugin — the unlock: the only file that touches `window`. A browser starts an audio
|
|
18903
|
+
* context suspended and lets it run on a user gesture, so two listeners wait for the first one.
|
|
18904
|
+
* They sit on `window`, not on the canvas: the gesture may as well be a button of the game's
|
|
18905
|
+
* loading page, which `input` never sees. A shell that asks for no gesture, such as a native
|
|
18906
|
+
* webview with the requirement switched off, lets the context run at once. So the plugin also
|
|
18907
|
+
* asks once at start, and unlocks with no touch where the answer is yes.
|
|
18576
18908
|
*/
|
|
18577
18909
|
/** The two gestures that unlock a context. The first of them to arrive wins. */
|
|
18578
18910
|
const UNLOCK_EVENTS = ["pointerdown", "touchend"];
|
|
18579
18911
|
/**
|
|
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:
|
|
18912
|
+
* Reacts to a resume that went through, of a gesture or of the start attempt. The context is
|
|
18913
|
+
* unlocked only when it really runs; the sounds fired during the resume and the track a scene
|
|
18914
|
+
* declared while it was locked start here. A context that still does not run drops the sounds:
|
|
18915
|
+
* the next gesture is a new moment. A gesture and the start attempt that settle together both
|
|
18916
|
+
* come here, and the music still starts once: a track that is on its way or already plays is not
|
|
18917
|
+
* started again. A later gesture on an unlocked context tries a remembered track again.
|
|
18583
18918
|
*
|
|
18584
18919
|
* @param ctx - Domain context of the plugin.
|
|
18585
18920
|
*/
|
|
@@ -18632,8 +18967,50 @@ function resumeContext(ctx) {
|
|
|
18632
18967
|
});
|
|
18633
18968
|
}
|
|
18634
18969
|
/**
|
|
18635
|
-
*
|
|
18636
|
-
* the context
|
|
18970
|
+
* Reacts to the start attempt that fulfilled. Nothing happens when the app stopped meanwhile, when
|
|
18971
|
+
* a gesture already unlocked the context, or when the context still does not run: the listeners
|
|
18972
|
+
* are there for that. Otherwise the listeners go and the context is unlocked the way a gesture
|
|
18973
|
+
* unlocks it.
|
|
18974
|
+
*
|
|
18975
|
+
* @param ctx - Domain context of the plugin.
|
|
18976
|
+
* @param context - The context the attempt was made on.
|
|
18977
|
+
*/
|
|
18978
|
+
function onStartResumed(ctx, context) {
|
|
18979
|
+
const state = ctx.state;
|
|
18980
|
+
if (state.context !== context || state.unlocked || context.state !== "running") return;
|
|
18981
|
+
removeUnlock(state);
|
|
18982
|
+
onResumed(ctx);
|
|
18983
|
+
}
|
|
18984
|
+
/**
|
|
18985
|
+
* Leaves a refused start attempt alone. Nobody asked for sound yet, so there is nothing to report,
|
|
18986
|
+
* and the listeners are installed: the first gesture asks again.
|
|
18987
|
+
*/
|
|
18988
|
+
function ignoreRefusal() {}
|
|
18989
|
+
/**
|
|
18990
|
+
* Unlocks the context in `onStart` where the shell lets audio play, and waits for a gesture where
|
|
18991
|
+
* it does not. A context that already runs is unlocked at once, with no listener. Any other gets
|
|
18992
|
+
* the two listeners and one `resume()`. `resuming` is not set for it: a browser that wants a
|
|
18993
|
+
* gesture leaves that promise pending, and a sound must not queue behind a promise that may never
|
|
18994
|
+
* settle. The attempt needs no `window`, so it also serves a context given through
|
|
18995
|
+
* `config.context` in a runtime without a DOM.
|
|
18996
|
+
*
|
|
18997
|
+
* @param ctx - Domain context of the plugin.
|
|
18998
|
+
*/
|
|
18999
|
+
function startUnlock(ctx) {
|
|
19000
|
+
const context = ctx.state.context;
|
|
19001
|
+
if (context === void 0) return;
|
|
19002
|
+
if (context.state === "running") {
|
|
19003
|
+
onResumed(ctx);
|
|
19004
|
+
return;
|
|
19005
|
+
}
|
|
19006
|
+
installUnlock(ctx);
|
|
19007
|
+
context.resume().then(() => {
|
|
19008
|
+
onStartResumed(ctx, context);
|
|
19009
|
+
}, ignoreRefusal);
|
|
19010
|
+
}
|
|
19011
|
+
/**
|
|
19012
|
+
* Installs the two unlock listeners on `window`. Without a DOM there is nothing to listen on:
|
|
19013
|
+
* the context stays locked unless the start attempt of `startUnlock` runs it.
|
|
18637
19014
|
*
|
|
18638
19015
|
* @param ctx - Domain context of the plugin.
|
|
18639
19016
|
*/
|
|
@@ -18720,9 +19097,10 @@ function createContext(config) {
|
|
|
18720
19097
|
}
|
|
18721
19098
|
/**
|
|
18722
19099
|
* 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
|
-
*
|
|
19100
|
+
* effect handlers, then the context with its graph and its unlock. The unlock asks the context to
|
|
19101
|
+
* run at once: a shell that lets audio play is unlocked with no touch, and a browser that wants a
|
|
19102
|
+
* gesture gets the two listeners. The handlers are registered even headless, so a node that
|
|
19103
|
+
* awaits a sound resolves in plain Bun exactly as it does in a browser.
|
|
18726
19104
|
*
|
|
18727
19105
|
* @param ctx - Kernel context of the audio plugin.
|
|
18728
19106
|
*/
|
|
@@ -18737,7 +19115,7 @@ function startAudio(ctx) {
|
|
|
18737
19115
|
state.context = context;
|
|
18738
19116
|
state.buses = buildGraph(context, state.buses);
|
|
18739
19117
|
applyAllGains(state);
|
|
18740
|
-
|
|
19118
|
+
startUnlock(audio);
|
|
18741
19119
|
}
|
|
18742
19120
|
/**
|
|
18743
19121
|
* Frees everything `onStart` opened: the two handler registrations, the two window listeners, the
|
|
@@ -24501,8 +24879,8 @@ function leave(ctx) {
|
|
|
24501
24879
|
* Runs the Back chain once. `back()` and the provider's `onBack` both go through here, so a test
|
|
24502
24880
|
* and a real press end the same way. Escape is what ui answers with the `escape` button of its
|
|
24503
24881
|
* 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.
|
|
24882
|
+
* is closed: it holds the answer for the next gate (one frame, or `flow.holdMs`), so the press
|
|
24883
|
+
* counts as taken and never leaves the app in the middle of a step.
|
|
24506
24884
|
*
|
|
24507
24885
|
* @param ctx - Kernel context of the platform plugin.
|
|
24508
24886
|
* @returns What took the press, `"none"` without a provider.
|
|
@@ -29945,9 +30323,10 @@ function restValueOf(current, value, owned) {
|
|
|
29945
30323
|
/**
|
|
29946
30324
|
* Records the rest pose of one extra component with a value, so a `change` hook can bring it
|
|
29947
30325
|
* home with `view.toRest`; a tag has no rest. The fields the type gives to a plugin are skipped.
|
|
30326
|
+
* The visual of a live element is recorded the same way each time its look is written.
|
|
29948
30327
|
*
|
|
29949
30328
|
* @param ctx - Domain context of the ui plugin.
|
|
29950
|
-
* @param element - The element that carries the
|
|
30329
|
+
* @param element - The element that carries the component.
|
|
29951
30330
|
* @param value - The value of this render.
|
|
29952
30331
|
*/
|
|
29953
30332
|
function recordRest(ctx, element, value) {
|
|
@@ -30915,14 +31294,17 @@ function createReconciler(ctx, modules, fields) {
|
|
|
30915
31294
|
* Writes the visual and input components of a live element again. A visual or an input the
|
|
30916
31295
|
* element no longer carries is removed, so a variant can trade the rectangle for a nine-slice and
|
|
30917
31296
|
* back, and a button that became disabled stops answering the gate. `Scroll` is only ever added:
|
|
30918
|
-
* its offset belongs to the finger, and a new look or a new rect never resets it. The
|
|
30919
|
-
* the
|
|
31297
|
+
* its offset belongs to the finger, and a new look or a new rect never resets it. The look
|
|
31298
|
+
* written is recorded as the rest of the visual, so the rest the world holds is always the one
|
|
31299
|
+
* the element is drawn with: a restore brings it back to exactly that. The extras of the
|
|
31300
|
+
* `components` prop are diffed last.
|
|
30920
31301
|
*
|
|
30921
31302
|
* @param element - The element that changed.
|
|
30922
31303
|
*/
|
|
30923
31304
|
function writeLive(element) {
|
|
30924
31305
|
writeOrder(element);
|
|
30925
|
-
const
|
|
31306
|
+
const visuals = visualOf$1(element);
|
|
31307
|
+
const values = [...visuals, ...inputOf(element)];
|
|
30926
31308
|
for (const type of [...VISUALS, ...INPUTS]) if (!values.some((value) => value.type === type) && ecs.has(element.entity, type)) ecs.remove(element.entity, type);
|
|
30927
31309
|
for (const value of values) {
|
|
30928
31310
|
if (!ecs.has(element.entity, value.type)) {
|
|
@@ -30933,6 +31315,7 @@ function createReconciler(ctx, modules, fields) {
|
|
|
30933
31315
|
const owned = ecs.typeOf(value.type.componentName)?.owned ?? [];
|
|
30934
31316
|
ecs.set(element.entity, asHandle(value.type), livePatch(value.value, owned));
|
|
30935
31317
|
}
|
|
31318
|
+
for (const value of visuals) recordRest(ctx, element, value);
|
|
30936
31319
|
writeExtras(element);
|
|
30937
31320
|
}
|
|
30938
31321
|
/**
|
|
@@ -31223,6 +31606,39 @@ function createReconciler(ctx, modules, fields) {
|
|
|
31223
31606
|
else modules.layout.enter(element);
|
|
31224
31607
|
}
|
|
31225
31608
|
/**
|
|
31609
|
+
* Asks for every root to be built again. The world calls it after a restore or a load, when it
|
|
31610
|
+
* brought every element to its rest pose and ended every track on it. The next reconcile does
|
|
31611
|
+
* the work, because only then the tree of the restored state is here to build from.
|
|
31612
|
+
*/
|
|
31613
|
+
function converged() {
|
|
31614
|
+
state.remount = true;
|
|
31615
|
+
ctx.deps.time.wake();
|
|
31616
|
+
}
|
|
31617
|
+
/**
|
|
31618
|
+
* Builds every root again from nothing: its element tree is dropped at once, with no exit, and
|
|
31619
|
+
* the reconcile of this pass enters it through the mount path. So after a restore or a load an
|
|
31620
|
+
* element stands exactly as if its root had just been mounted with the restored state: a new
|
|
31621
|
+
* entity with the components of the description, then its `enter` hook, then its loop. What a
|
|
31622
|
+
* mount does not bring is gone with the old tree: the local state of a component, a scroll
|
|
31623
|
+
* offset, the keyboard focus, a text being edited, a hover. A root that is leaving keeps
|
|
31624
|
+
* leaving.
|
|
31625
|
+
*/
|
|
31626
|
+
function remountRoots() {
|
|
31627
|
+
state.remount = false;
|
|
31628
|
+
for (const root of state.roots.values()) {
|
|
31629
|
+
if (state.removing.has(root.entity)) continue;
|
|
31630
|
+
const element = root.element === void 0 ? void 0 : lookup(root.element);
|
|
31631
|
+
if (element !== void 0) {
|
|
31632
|
+
forgetSubtree(element);
|
|
31633
|
+
forgetInstances(state, element.identity);
|
|
31634
|
+
despawnTree(element);
|
|
31635
|
+
}
|
|
31636
|
+
root.element = void 0;
|
|
31637
|
+
root.dirty = true;
|
|
31638
|
+
root.needsSolve = true;
|
|
31639
|
+
}
|
|
31640
|
+
}
|
|
31641
|
+
/**
|
|
31226
31642
|
* Reconciles one root from the node its `Tree` carries.
|
|
31227
31643
|
*
|
|
31228
31644
|
* @param root - The root to reconcile.
|
|
@@ -31283,13 +31699,15 @@ function createReconciler(ctx, modules, fields) {
|
|
|
31283
31699
|
}
|
|
31284
31700
|
}
|
|
31285
31701
|
/**
|
|
31286
|
-
* The first of the two systems of phase `layout`: the sweep, the
|
|
31287
|
-
* pull of the text being typed, the scroll offsets and the
|
|
31288
|
-
* show a string of another size, the viewport, the dirty
|
|
31702
|
+
* The first of the two systems of phase `layout`: the sweep, the roots a restore asked to build
|
|
31703
|
+
* again, the popups that may leave, the pull of the text being typed, the scroll offsets and the
|
|
31704
|
+
* windowed ranges, the bound texts that show a string of another size, the viewport, the dirty
|
|
31705
|
+
* roots and the hosted views.
|
|
31289
31706
|
*/
|
|
31290
31707
|
function reconcile() {
|
|
31291
31708
|
sweep();
|
|
31292
31709
|
state.reconciles += 1;
|
|
31710
|
+
if (state.remount) remountRoots();
|
|
31293
31711
|
settlePopups(ctx, state, unmountRoot);
|
|
31294
31712
|
fields.pull();
|
|
31295
31713
|
const containers = scrollContainers();
|
|
@@ -31429,7 +31847,8 @@ function createReconciler(ctx, modules, fields) {
|
|
|
31429
31847
|
unmountRoot,
|
|
31430
31848
|
markPointer,
|
|
31431
31849
|
playEnter,
|
|
31432
|
-
sweep
|
|
31850
|
+
sweep,
|
|
31851
|
+
converged
|
|
31433
31852
|
};
|
|
31434
31853
|
}
|
|
31435
31854
|
//#endregion
|
|
@@ -31555,6 +31974,7 @@ function createJsxApi(ctx, modules) {
|
|
|
31555
31974
|
applyTap: (entity) => applyTap(ctx, entity),
|
|
31556
31975
|
markPointer: frame.markPointer,
|
|
31557
31976
|
playEnter: frame.playEnter,
|
|
31977
|
+
converged: frame.converged,
|
|
31558
31978
|
key: (input) => focus.key(input),
|
|
31559
31979
|
blur: focus.blur
|
|
31560
31980
|
};
|
|
@@ -31828,7 +32248,9 @@ function restVisual(element) {
|
|
|
31828
32248
|
}
|
|
31829
32249
|
/**
|
|
31830
32250
|
* Records the rest pose of one element before a hook runs: the transform from `Box` and the
|
|
31831
|
-
* style, and the visual the element is actually drawn with.
|
|
32251
|
+
* style, and the visual the element is actually drawn with. The content of a scroll records no
|
|
32252
|
+
* rest `Transform`: the scroll step owns its pose, and the world brings an element back to every
|
|
32253
|
+
* rest it holds after a restore.
|
|
31832
32254
|
*
|
|
31833
32255
|
* @param ctx - Domain context of the ui plugin.
|
|
31834
32256
|
* @param element - The element that entered or moved.
|
|
@@ -31836,7 +32258,7 @@ function restVisual(element) {
|
|
|
31836
32258
|
*/
|
|
31837
32259
|
function writeRest(ctx, element, parent) {
|
|
31838
32260
|
const projection = ctx.deps.world.projection;
|
|
31839
|
-
projection.setRest(element.entity, Transform, restTransform(element.rect, parent, element.style, element.fit));
|
|
32261
|
+
if (element.type !== "content") projection.setRest(element.entity, Transform, restTransform(element.rect, parent, element.style, element.fit));
|
|
31840
32262
|
const visual = restVisual(element);
|
|
31841
32263
|
if (visual !== void 0) projection.setRest(element.entity, visual.component, visual.value);
|
|
31842
32264
|
}
|
|
@@ -31948,8 +32370,8 @@ function still(element) {
|
|
|
31948
32370
|
* element plays its `change.Transform` motion when it has one, else takes the pose through a
|
|
31949
32371
|
* 0 ms rest track, which lands on the next frame step and lets an additive loop re-base on the
|
|
31950
32372
|
* new rest; an element that has not spawned yet only records it, and so does one whose
|
|
31951
|
-
* `change.Box` motion already played. The content of a scroll keeps its `Transform
|
|
31952
|
-
* step owns it.
|
|
32373
|
+
* `change.Box` motion already played. The content of a scroll keeps its `Transform` and records
|
|
32374
|
+
* no rest with the world: the scroll step owns it.
|
|
31953
32375
|
*
|
|
31954
32376
|
* @param ctx - Domain context of the ui plugin.
|
|
31955
32377
|
* @param element - The element whose rect, style or fit may have changed.
|
|
@@ -31962,6 +32384,7 @@ function repose(ctx, element, parent, hooked, hint) {
|
|
|
31962
32384
|
if (samePose(element.rest, next)) return;
|
|
31963
32385
|
const previous = element.rest;
|
|
31964
32386
|
element.rest = next;
|
|
32387
|
+
if (element.type === "content") return;
|
|
31965
32388
|
ctx.deps.world.projection.setRest(element.entity, Transform, next);
|
|
31966
32389
|
if (!element.live || hooked) return;
|
|
31967
32390
|
const hook = element.motion?.change?.Transform;
|
|
@@ -31969,7 +32392,7 @@ function repose(ctx, element, parent, hooked, hint) {
|
|
|
31969
32392
|
play(ctx, element, (view) => hook(view, previous, next, hint));
|
|
31970
32393
|
return;
|
|
31971
32394
|
}
|
|
31972
|
-
|
|
32395
|
+
handleOf(ctx, element)?.toRest(Transform, { ms: 0 });
|
|
31973
32396
|
}
|
|
31974
32397
|
/**
|
|
31975
32398
|
* Moves the rest `Transform` of a root element up by the keyboard lift, or back to its own rest
|
|
@@ -33401,7 +33824,8 @@ function registerComponents(ctx, jsx) {
|
|
|
33401
33824
|
}
|
|
33402
33825
|
/**
|
|
33403
33826
|
* Opens the two systems of phase `layout`, the seven world hooks of `Tree`, `Box`, `Pressed` and
|
|
33404
|
-
* `PointerOver`, the
|
|
33827
|
+
* `PointerOver`, the listener that asks for every root to be built again after a restore or a
|
|
33828
|
+
* load, the two effect handlers, the hint listener that buffers every released hint for
|
|
33405
33829
|
* the `change` hooks of the next frame step, the three tap listeners (the `LocalWrite` patch, the
|
|
33406
33830
|
* pointer tap that clears the keyboard focus, and the tap that starts or ends the editing of a
|
|
33407
33831
|
* text field), the pointer door that opens the keyboard inside the DOM listener, the key listener
|
|
@@ -33429,7 +33853,7 @@ function openRegistrations(ctx, jsx, layout) {
|
|
|
33429
33853
|
})), ecs.onAdded(Tree, (entity) => {
|
|
33430
33854
|
const place = projection.keyOf(entity);
|
|
33431
33855
|
jsx.mountRoot(entity, place?.projection ?? "ui", ecs.get(entity, Layer)?.name ?? "ui");
|
|
33432
|
-
}), ecs.onRemoved(Tree, (entity) => jsx.unmountRoot(entity)), ecs.onAdded(Box, (entity) => jsx.playEnter(entity)), ecs.onAdded(Pressed, (entity) => jsx.markPointer(entity, "pressed", true)), ecs.onRemoved(Pressed, (entity) => jsx.markPointer(entity, "pressed", false)), ecs.onAdded(PointerOver, (entity) => jsx.markPointer(entity, "hover", true)), ecs.onRemoved(PointerOver, (entity) => jsx.markPointer(entity, "hover", false)), ctx.deps.flow.fx.handle("popup", layout.popupHandler(jsx)), ctx.deps.flow.fx.handle("guide", layout.guideHandler()), ctx.deps.flow.fx.onHint((hint) => {
|
|
33856
|
+
}), ecs.onRemoved(Tree, (entity) => jsx.unmountRoot(entity)), ecs.onAdded(Box, (entity) => jsx.playEnter(entity)), projection.onConverged(jsx.converged), ecs.onAdded(Pressed, (entity) => jsx.markPointer(entity, "pressed", true)), ecs.onRemoved(Pressed, (entity) => jsx.markPointer(entity, "pressed", false)), ecs.onAdded(PointerOver, (entity) => jsx.markPointer(entity, "hover", true)), ecs.onRemoved(PointerOver, (entity) => jsx.markPointer(entity, "hover", false)), ctx.deps.flow.fx.handle("popup", layout.popupHandler(jsx)), ctx.deps.flow.fx.handle("guide", layout.guideHandler()), ctx.deps.flow.fx.onHint((hint) => {
|
|
33433
33857
|
ctx.state.jsx.hints.push(hint);
|
|
33434
33858
|
}), ctx.deps.input.onTap(jsx.applyTap), ctx.deps.input.onTap(() => jsx.blur()), ctx.deps.input.onTap(jsx.tapped), ctx.deps.input.onPointer(jsx.pointer), ctx.deps.input.onKey(jsx.key), ctx.deps.input.controls.add(LocalWrite), () => jsx.releaseHosted(), () => ecs.despawnOwnedBy(UI_OWNER));
|
|
33435
33859
|
}
|
|
@@ -33482,6 +33906,7 @@ function stopUi(state) {
|
|
|
33482
33906
|
state.jsx.hosts.clear();
|
|
33483
33907
|
state.jsx.hosted.clear();
|
|
33484
33908
|
state.jsx.scrolls.clear();
|
|
33909
|
+
state.jsx.remount = false;
|
|
33485
33910
|
state.jsx.hints.length = 0;
|
|
33486
33911
|
state.jsx.focus.entity = void 0;
|
|
33487
33912
|
state.jsx.focus.ring = void 0;
|
|
@@ -33824,7 +34249,8 @@ function createJsxState() {
|
|
|
33824
34249
|
text: createTextState(),
|
|
33825
34250
|
hints: [],
|
|
33826
34251
|
scrolls: emptySet(),
|
|
33827
|
-
windowRenders: 0
|
|
34252
|
+
windowRenders: 0,
|
|
34253
|
+
remount: false
|
|
33828
34254
|
};
|
|
33829
34255
|
}
|
|
33830
34256
|
//#endregion
|
|
@@ -35424,6 +35850,7 @@ function defineMotion(spec) {
|
|
|
35424
35850
|
* | flow | retries | 1 |
|
|
35425
35851
|
* | flow | settleTimeoutMs | 2000 |
|
|
35426
35852
|
* | flow | journalLimit | 500 |
|
|
35853
|
+
* | flow | holdMs | 0, an answer to a closed gate lives one frame |
|
|
35427
35854
|
* | world | settleMs | 350 |
|
|
35428
35855
|
* | world | reconciledEvent | false |
|
|
35429
35856
|
* | renderer | mount | undefined, inert without a mount |
|