fyflow-orchestration 0.8.0 → 0.9.0
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/AGENTS.md +96 -7
- package/README.md +2 -1
- package/dist/browser/chunks/{chunk-EUU44PHD.js → chunk-3U4UZC34.js} +642 -229
- package/dist/browser/index.js +1 -1
- package/dist/browser/scheduler/index.js +61 -13
- package/dist/node/chunks/{chunk-EUU44PHD.js → chunk-3U4UZC34.js} +642 -229
- package/dist/node/index.js +1 -1
- package/dist/node/scheduler/index.js +61 -13
- package/dist/types/src/driver/driver.d.ts +55 -21
- package/dist/types/src/fold/accumulator.d.ts +94 -0
- package/dist/types/src/fold/run-state.d.ts +30 -3
- package/dist/types/src/scheduler/runtime.d.ts +39 -7
- package/package.json +1 -1
package/dist/node/index.js
CHANGED
|
@@ -143,7 +143,7 @@ import {
|
|
|
143
143
|
violatedRules,
|
|
144
144
|
witness,
|
|
145
145
|
workersOf
|
|
146
|
-
} from "./chunks/chunk-
|
|
146
|
+
} from "./chunks/chunk-3U4UZC34.js";
|
|
147
147
|
|
|
148
148
|
// src/definition/assist.ts
|
|
149
149
|
var TYPES = ["object", "array", "string", "number", "integer", "boolean", "null"];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Accumulator,
|
|
2
3
|
BUILT_IN_NODE_TYPES,
|
|
3
4
|
ConfigRefused,
|
|
4
5
|
Driver,
|
|
@@ -22,10 +23,9 @@ import {
|
|
|
22
23
|
resolveRegistry,
|
|
23
24
|
resumeRefusal,
|
|
24
25
|
retryPolicyProblem,
|
|
25
|
-
step,
|
|
26
26
|
validateDefinition,
|
|
27
27
|
workersOf
|
|
28
|
-
} from "../chunks/chunk-
|
|
28
|
+
} from "../chunks/chunk-3U4UZC34.js";
|
|
29
29
|
|
|
30
30
|
// src/scheduler/bootstrap.ts
|
|
31
31
|
import { WorkerManager } from "fyflow-scheduler";
|
|
@@ -281,14 +281,50 @@ function createRuntime(document, code = {}, options = {}) {
|
|
|
281
281
|
const done = new Promise((resolve) => {
|
|
282
282
|
settle = resolve;
|
|
283
283
|
});
|
|
284
|
-
|
|
284
|
+
const acc = Accumulator.load(emptyState());
|
|
285
|
+
let cached = null;
|
|
286
|
+
const frozen = /* @__PURE__ */ new WeakSet();
|
|
285
287
|
let settled = false;
|
|
286
288
|
let seed = from === null ? null : from.events;
|
|
289
|
+
const own = (input) => {
|
|
290
|
+
if (input.kind !== "outcome" && input.kind !== "resolution") return input;
|
|
291
|
+
const r = input.record;
|
|
292
|
+
return typeof r.result === "object" && r.result !== null ? { kind: input.kind, record: structuredClone(r) } : input;
|
|
293
|
+
};
|
|
294
|
+
const take = (input) => {
|
|
295
|
+
if (acc.apply(own(input))) cached = null;
|
|
296
|
+
};
|
|
297
|
+
const catchUp = () => {
|
|
298
|
+
if (seed === null) return;
|
|
299
|
+
for (const stored of seed) take(stored);
|
|
300
|
+
seed = null;
|
|
301
|
+
};
|
|
302
|
+
const freezeDeep = (root) => {
|
|
303
|
+
const stack = [root];
|
|
304
|
+
while (stack.length > 0) {
|
|
305
|
+
const v = stack.pop();
|
|
306
|
+
if (typeof v !== "object" || v === null || frozen.has(v)) continue;
|
|
307
|
+
frozen.add(v);
|
|
308
|
+
if (ArrayBuffer.isView(v)) continue;
|
|
309
|
+
if (v instanceof Map) for (const [k, x] of v) stack.push(k, x);
|
|
310
|
+
else if (v instanceof Set) for (const x of v) stack.push(x);
|
|
311
|
+
for (const k of Reflect.ownKeys(v)) {
|
|
312
|
+
const d = Object.getOwnPropertyDescriptor(v, k);
|
|
313
|
+
if (d !== void 0 && "value" in d) stack.push(d.value);
|
|
314
|
+
}
|
|
315
|
+
Object.freeze(v);
|
|
316
|
+
}
|
|
317
|
+
return root;
|
|
318
|
+
};
|
|
319
|
+
const current = () => {
|
|
320
|
+
catchUp();
|
|
321
|
+
return cached ??= freezeDeep(acc.materialise());
|
|
322
|
+
};
|
|
287
323
|
const settleIfTerminal = () => {
|
|
288
|
-
if (settled || !isTerminal(
|
|
324
|
+
if (settled || !isTerminal({ outcome: acc.outcomeNow() })) return;
|
|
289
325
|
settled = true;
|
|
290
|
-
|
|
291
|
-
|
|
326
|
+
const at = current();
|
|
327
|
+
if (at.outcome === "interrupted" && stopping.size > 0) {
|
|
292
328
|
const fin = () => {
|
|
293
329
|
settle(at);
|
|
294
330
|
router.release(runId);
|
|
@@ -296,7 +332,7 @@ function createRuntime(document, code = {}, options = {}) {
|
|
|
296
332
|
void Promise.allSettled([...stopping]).then(fin);
|
|
297
333
|
return;
|
|
298
334
|
}
|
|
299
|
-
settle(
|
|
335
|
+
settle(at);
|
|
300
336
|
router.release(runId);
|
|
301
337
|
};
|
|
302
338
|
const view = router.forRun(runId);
|
|
@@ -333,11 +369,8 @@ function createRuntime(document, code = {}, options = {}) {
|
|
|
333
369
|
// of a run from its first, and it is the well-formed wrong answer the first time a driver's
|
|
334
370
|
// sequence does not start at this callback's first call.
|
|
335
371
|
onFoldInput: (input, seq) => {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
seed = null;
|
|
339
|
-
}
|
|
340
|
-
state = step(state, input);
|
|
372
|
+
catchUp();
|
|
373
|
+
take(input);
|
|
341
374
|
queueMicrotask(settleIfTerminal);
|
|
342
375
|
startOptions.onFoldInput?.(input, seq);
|
|
343
376
|
},
|
|
@@ -361,7 +394,22 @@ function createRuntime(document, code = {}, options = {}) {
|
|
|
361
394
|
router.release(runId);
|
|
362
395
|
throw e;
|
|
363
396
|
}
|
|
364
|
-
return {
|
|
397
|
+
return {
|
|
398
|
+
driver,
|
|
399
|
+
done,
|
|
400
|
+
header: driver.checkpoint().header,
|
|
401
|
+
stop: (stopOptions) => driver.stop(stopOptions),
|
|
402
|
+
state: current,
|
|
403
|
+
outcome: () => {
|
|
404
|
+
catchUp();
|
|
405
|
+
return acc.outcomeNow();
|
|
406
|
+
},
|
|
407
|
+
node: (taskId) => {
|
|
408
|
+
catchUp();
|
|
409
|
+
const entry = acc.entry(taskId);
|
|
410
|
+
return entry === void 0 ? null : freezeDeep(entry);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
365
413
|
}
|
|
366
414
|
}
|
|
367
415
|
function settlementRouter(scheduler) {
|
|
@@ -939,13 +939,21 @@ export declare class Driver {
|
|
|
939
939
|
* channels with no shared order, so `RN-R3`'s *"read in append order"* was about
|
|
940
940
|
* a value no one could hold.
|
|
941
941
|
*
|
|
942
|
-
* `log` above is the **record-kind subsequence of this array**,
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
942
|
+
* `log` above is the **record-kind subsequence of this array**, by reference — the
|
|
943
|
+
* same record objects, never copies; `test/driver/` asserts the two agree, so the
|
|
944
|
+
* redundancy is checked rather than trusted. *(It was kept "only because
|
|
945
|
+
* `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
|
|
946
|
+
* whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
|
|
947
|
+
* It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
|
|
948
|
+
* catch-up, and the retention walk's alias check.)*
|
|
947
949
|
*/
|
|
948
950
|
private readonly inputs;
|
|
951
|
+
/**
|
|
952
|
+
* **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
|
|
953
|
+
* `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
|
|
954
|
+
* `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
|
|
955
|
+
*/
|
|
956
|
+
private readonly taskLog;
|
|
949
957
|
private readonly graph;
|
|
950
958
|
private readonly regions;
|
|
951
959
|
private readonly dispatched;
|
|
@@ -1687,7 +1695,7 @@ export declare class Driver {
|
|
|
1687
1695
|
* [ADR 0040](../../doc/decisions/0040-what-a-node-can-reach-is-the-runs-own-outputs-and-a-node-id-names-them-relative-to-a-branch-path.md)
|
|
1688
1696
|
* rules 2 and 3, and `FD-C2`.
|
|
1689
1697
|
*
|
|
1690
|
-
* **It is a PROJECTION of the log and adds no retention** — measured at
|
|
1698
|
+
* **It is a PROJECTION of the log and adds no retention of a result** — measured at
|
|
1691
1699
|
* [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
|
|
1692
1700
|
* declaration and again before this was written: this driver holds
|
|
1693
1701
|
* `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
|
|
@@ -1697,14 +1705,11 @@ export declare class Driver {
|
|
|
1697
1705
|
* described **today** before this method existed, and this adds **no retention**.
|
|
1698
1706
|
* *(Q19 is RESOLVED since W53 —
|
|
1699
1707
|
* [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
|
|
1700
|
-
* — and `RN-R13` names
|
|
1708
|
+
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
|
|
1701
1709
|
* asserted by IDENTITY at ladder L2 rather than by a byte count, because a
|
|
1702
1710
|
* reachability SET cannot tell an alias from a copy. W53's readers found that
|
|
1703
1711
|
* from both ends — the rule's stated test returned a third name on a `needs`
|
|
1704
1712
|
* flow, and cloning here cost ZERO.)*
|
|
1705
|
-
* *(It adds ONE field — `outputsCache` below — which is a memo the log invalidates
|
|
1706
|
-
* and which holds nothing the log does not. This read *"zero fields"* until W41's
|
|
1707
|
-
* conformity reader read it beside the memo the cost measurement had just added.)*
|
|
1708
1713
|
*
|
|
1709
1714
|
* **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
|
|
1710
1715
|
* section 3 rule 4's own reason: a resumed node has two records in the log and
|
|
@@ -1712,19 +1717,46 @@ export declare class Driver {
|
|
|
1712
1717
|
* suspended has no effective outcome and therefore no entry — which is what makes
|
|
1713
1718
|
* `FD-R3` fire rather than a reference quietly resolving to `undefined`.
|
|
1714
1719
|
*
|
|
1715
|
-
* **
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
* `
|
|
1720
|
-
*
|
|
1720
|
+
* **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
|
|
1721
|
+
* [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
|
|
1722
|
+
* rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
|
|
1723
|
+
* WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
|
|
1724
|
+
* of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
|
|
1725
|
+
* construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
|
|
1726
|
+
* `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
|
|
1727
|
+
* ONE place, which is ADR 0040 rule 2's own reason, and an entry is always the merge of its task's whole sub-log at
|
|
1728
|
+
* the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
|
|
1729
|
+
* (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
|
|
1730
|
+
* in its tasks' first-record order, each `result` the log record's own object.
|
|
1731
|
+
*
|
|
1732
|
+
* **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
|
|
1733
|
+
* node or an edge that declares a `needs` asks, and a run that declares none never builds it.
|
|
1721
1734
|
*/
|
|
1722
1735
|
private outputs;
|
|
1723
1736
|
/**
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1737
|
+
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
1738
|
+
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
1739
|
+
* in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
|
|
1740
|
+
*
|
|
1741
|
+
* **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
|
|
1742
|
+
* RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
|
|
1743
|
+
* at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
|
|
1744
|
+
* a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
|
|
1745
|
+
* `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
|
|
1746
|
+
* fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
|
|
1747
|
+
* that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
|
|
1748
|
+
* so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
|
|
1749
|
+
* none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
|
|
1750
|
+
*/
|
|
1751
|
+
private place;
|
|
1752
|
+
/**
|
|
1753
|
+
* The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
|
|
1754
|
+
* it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
|
|
1755
|
+
* never a copy — `tasks` is one slot per task id
|
|
1756
|
+
* read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
|
|
1757
|
+
* node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
|
|
1758
|
+
* read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
|
|
1759
|
+
* *the driver computes no run state* is untouched. `null` until a `needs` first asks.
|
|
1728
1760
|
*/
|
|
1729
1761
|
private outputsCache;
|
|
1730
1762
|
/**
|
|
@@ -1980,7 +2012,9 @@ export declare class Driver {
|
|
|
1980
2012
|
*
|
|
1981
2013
|
* A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
|
|
1982
2014
|
* the stored object and `OR-R8` derives rather than appends, so a second copy
|
|
1983
|
-
* here would be a second truth.
|
|
2015
|
+
* here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
|
|
2016
|
+
* by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
|
|
2017
|
+
* element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
|
|
1984
2018
|
* copy, frozen, for the same reason `snapshot()` is: a listener holding the
|
|
1985
2019
|
* driver's own array could forge a settlement and bypass `RS-R1`.
|
|
1986
2020
|
*/
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { FoldInput, NodeRun, RunOutcome, RunState } from './run-state.js';
|
|
2
|
+
/**
|
|
3
|
+
* **A count of the work an accumulator did** — P39 decision 6 and stop-and-ask (e): one per input applied, and one per
|
|
4
|
+
* entry, element or cell it reads or writes while loading, copying or materialising. Clock-free, so a ratio of two
|
|
5
|
+
* counts is a property of the code and not of a machine (Q179). Owned by the caller; absent, nothing is counted.
|
|
6
|
+
*/
|
|
7
|
+
export interface FoldCounter {
|
|
8
|
+
visits: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class Accumulator {
|
|
11
|
+
private runId;
|
|
12
|
+
private outcome;
|
|
13
|
+
private haltedBy;
|
|
14
|
+
private cancelRequest;
|
|
15
|
+
private interruption;
|
|
16
|
+
private paused;
|
|
17
|
+
/** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
|
|
18
|
+
private readonly nodes;
|
|
19
|
+
/** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
|
|
20
|
+
private readonly regions;
|
|
21
|
+
private readonly pathEnds;
|
|
22
|
+
private readonly resumes;
|
|
23
|
+
private readonly gated;
|
|
24
|
+
private counts;
|
|
25
|
+
private readonly base;
|
|
26
|
+
private readonly counter;
|
|
27
|
+
private constructor();
|
|
28
|
+
/** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
|
|
29
|
+
static load(state: RunState, counter?: FoldCounter): Accumulator;
|
|
30
|
+
private tick;
|
|
31
|
+
/**
|
|
32
|
+
* **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
|
|
33
|
+
* O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
|
|
34
|
+
*/
|
|
35
|
+
outcomeNow(): RunOutcome;
|
|
36
|
+
/**
|
|
37
|
+
* **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
|
|
38
|
+
* An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
|
|
39
|
+
* dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
|
|
40
|
+
*/
|
|
41
|
+
entry(taskId: string): NodeRun | undefined;
|
|
42
|
+
/** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
|
|
43
|
+
private node;
|
|
44
|
+
/** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
|
|
45
|
+
private instance;
|
|
46
|
+
/** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
|
|
47
|
+
private tally;
|
|
48
|
+
private countNode;
|
|
49
|
+
private stateCount;
|
|
50
|
+
/** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
|
|
51
|
+
private setNode;
|
|
52
|
+
/**
|
|
53
|
+
* The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
|
|
54
|
+
* paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
|
|
55
|
+
*/
|
|
56
|
+
private touchRegion;
|
|
57
|
+
private unweigh;
|
|
58
|
+
/** The instance's share of the region counts put back after a write. */
|
|
59
|
+
private settleRegion;
|
|
60
|
+
private addPathEnd;
|
|
61
|
+
/**
|
|
62
|
+
* **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
|
|
63
|
+
* `false` where it returned the state it was handed, so `step` can return that same object. Throws where
|
|
64
|
+
* that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
|
|
65
|
+
* the module header's stated limit on region paths.
|
|
66
|
+
*/
|
|
67
|
+
apply(input: FoldInput): boolean;
|
|
68
|
+
private applyRegion;
|
|
69
|
+
/**
|
|
70
|
+
* **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
|
|
71
|
+
* one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
|
|
72
|
+
* recomputes has moved the state.
|
|
73
|
+
*/
|
|
74
|
+
private withOutcome;
|
|
75
|
+
private precedence;
|
|
76
|
+
/**
|
|
77
|
+
* **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
|
|
78
|
+
* the base's own object, and one that was written is a new one with the base's entries in their places and the new
|
|
79
|
+
* entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
|
|
80
|
+
*/
|
|
81
|
+
materialise(): RunState;
|
|
82
|
+
private list;
|
|
83
|
+
private materialiseRegion;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* **The fold, with its work counted** — the INTERNAL entry point P39 decision 6 names: `fold` is this with no counter.
|
|
87
|
+
* One accumulator, every input applied in append order (`RN-R3`), materialised once: linear in `inputs`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function foldWith(inputs: readonly FoldInput[], counter?: FoldCounter): RunState;
|
|
90
|
+
/**
|
|
91
|
+
* **One step, with its work counted** — `step` is this with no counter: load, apply, materialise, and the state it was
|
|
92
|
+
* handed itself back wherever the arm moves nothing. O(the state it is handed).
|
|
93
|
+
*/
|
|
94
|
+
export declare function stepWith(prev: RunState, input: FoldInput, counter?: FoldCounter): RunState;
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* ladder L3 checks by folding the same log twice and deep-comparing, and it is
|
|
7
7
|
* what makes [Q14](../../doc/questions/q14.md)'s answer an answer rather than a
|
|
8
8
|
* restated leaning.
|
|
9
|
+
*
|
|
10
|
+
* **Since v0.29 the arms live in ONE place, `src/fold/accumulator.ts`** (`RN-R43`,
|
|
11
|
+
* [ADR 0120](../../doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md)): an accumulator mutated in
|
|
12
|
+
* place WITHIN one call and materialised into a plain state, so nothing survives a call and `RN-R1` holds as before.
|
|
13
|
+
* This module keeps the types, `emptyState`, `isTerminal`, `nodeOf` and `foldLog`, and `fold` and `step` call into it.
|
|
14
|
+
* That module is internal: `index.ts` re-exports this one and never it.
|
|
9
15
|
*/
|
|
10
16
|
import type { LogRecord, OutcomeRecord } from '../driver/log.js';
|
|
11
17
|
import type { RegionEvent } from '../driver/events.js';
|
|
@@ -491,14 +497,35 @@ export type FoldInput = {
|
|
|
491
497
|
};
|
|
492
498
|
export declare function emptyState(): RunState;
|
|
493
499
|
/**
|
|
494
|
-
* One step. **
|
|
495
|
-
* what lets `RN-R2` hold (folding a prefix then one more record equals folding the
|
|
500
|
+
* One step. **Never mutates the state it was given, and returns a NEW one wherever the input moves something** — and
|
|
501
|
+
* that state ITSELF where it moves nothing (below) — which is what lets `RN-R2` hold (folding a prefix then one more record equals folding the
|
|
496
502
|
* whole) and what makes a fold usable while a run is still going.
|
|
503
|
+
*
|
|
504
|
+
* **Its work is O(the state it is handed), not O(the input)** — `RN-R43`, v0.29,
|
|
505
|
+
* [ADR 0120](../../doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md): it loads `prev` into the
|
|
506
|
+
* fold's one accumulator, applies the input and materialises a new plain state, and a pure function from one plain
|
|
507
|
+
* `RunState` to another cannot do less than copy the record it changes. So stepping every input of a long run costs
|
|
508
|
+
* O(run) per input; {@link fold} over the whole sequence is linear. A consumer that needs a run's state after every input
|
|
509
|
+
* reads it where a runtime keeps it, rather than stepping a copy of its own.
|
|
510
|
+
*
|
|
511
|
+
* **Each arm behaves on ANY state it is handed exactly as it did before v0.29** — not only on one a fold can reach. The
|
|
512
|
+
* outcome is recomputed only by the arms that recomputed it, so a hand-built state whose `outcome` disagrees with its
|
|
513
|
+
* cells keeps that outcome across `paused`, `unpaused`, `gated`, `attempted` and a `resumed` that ends no stop; and an
|
|
514
|
+
* input that moves nothing — a second cancel request, or one after the run is already `cancelled`, a second stop or
|
|
515
|
+
* pause, an unpause of a run not paused, a dispatch or an ask of a
|
|
516
|
+
* task already in `nodes`, a region instance already entered, an attempt of a task not in `nodes` — hands back `prev`
|
|
517
|
+
* ITSELF. A cell no input touched is `prev`'s own object in the result. (Its one stated limit, a region path naming an
|
|
518
|
+
* `Object.prototype` member, which no driver appends, is `src/fold/accumulator.ts`'s header's.)
|
|
497
519
|
*/
|
|
498
520
|
export declare function step(prev: RunState, input: FoldInput): RunState;
|
|
499
521
|
/**
|
|
500
522
|
* The fold. `RN-R2`: total over any prefix, so `fold(log.slice(0, n))` is defined
|
|
501
|
-
* for every `n` and `step(fold(prefix), next)
|
|
523
|
+
* for every `n` and `step(fold(prefix), next)` deep-equals `fold(prefix + next)`.
|
|
524
|
+
*
|
|
525
|
+
* **Linear in its inputs** — `RN-R43`, v0.29, [W194](../../doc/work-items/w194-the-fold-is-one-accumulator.md): ONE
|
|
526
|
+
* accumulator (`src/fold/accumulator.ts`, internal) has every input applied to it in place and is materialised into a
|
|
527
|
+
* plain state once, at the end. Before v0.29 this was a `step` per input, each copying the run, so a fold was quadratic
|
|
528
|
+
* in its run — 30 s for a 4,000-item map (P39 fact 1). What it returns is unchanged on every sequence whose region paths are branch paths (P39 decision 5; `RN-R43`'s one stated limit).
|
|
502
529
|
*
|
|
503
530
|
* `RN-R3`: read in **append order**, never sorted. Two settlements can share a
|
|
504
531
|
* millisecond; `schedulerTimestamp` is the scheduler's clock and `observedAt` is
|
|
@@ -65,7 +65,7 @@ import { FyflowScheduler } from 'fyflow-scheduler';
|
|
|
65
65
|
import type { ResourceGroup, WorkerManager } from 'fyflow-scheduler';
|
|
66
66
|
import { Driver } from '../driver/driver.js';
|
|
67
67
|
import type { Checkpoint, CheckpointHeader, DriverOptions, RunStopOptions } from '../driver/driver.js';
|
|
68
|
-
import type { RunState } from '../fold/run-state.js';
|
|
68
|
+
import type { NodeRun, RunOutcome, RunState } from '../fold/run-state.js';
|
|
69
69
|
import type { Finding, FlowDefinition, JsonSchema, RegistryCode, RegistryDocument, ResolvedPool, ResolvedRegistry, RetryPolicy, TaskNode } from '../definition/types.js';
|
|
70
70
|
import type { PoolFactory } from './bootstrap.js';
|
|
71
71
|
/**
|
|
@@ -291,12 +291,17 @@ export interface RunHandle {
|
|
|
291
291
|
* element: the stopped driver is deaf, so no settlement could move the fold again, and the runtime waits on the cancels
|
|
292
292
|
* the driver sent through this run's view of the scheduler instead (`RN-R33`, ADR 0105, amending ADR 0058 rule 3).
|
|
293
293
|
*
|
|
294
|
-
* It is the SAME expression a consumer would have written, moved: the runtime keeps ONE
|
|
295
|
-
* `
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* `fold(driver.foldInputs())` — the caller's own
|
|
299
|
-
* is a second route rather than a second definition.
|
|
294
|
+
* It is the SAME expression a consumer would have written, moved: the runtime keeps the fold's ONE accumulator OPEN —
|
|
295
|
+
* W194's, `fold`'s own definition (`RN-R43`) — and applies each fold input to it, which is what `fold` does over a
|
|
296
|
+
* whole sequence; `RN-R2` is the rule that says a prefix applied equals a fold of that prefix. *(Until W196 it kept ONE
|
|
297
|
+
* `RunState` and `step`ped it per input — O(run) a step since W194, so O(run²) a run; ADR 0122 amends ADR 0058 rule 5.)*
|
|
298
|
+
* `test/scheduler/runtime.test.ts` holds `await done` equal to `fold(driver.foldInputs())` — the caller's own
|
|
299
|
+
* expression over the driver's own sequence, which is a second route rather than a second definition.
|
|
300
|
+
*
|
|
301
|
+
* **It resolves with the SAME frozen object {@link RunHandle.state} returns at that moment** — P39 (f), `RN-R44`: one
|
|
302
|
+
* copy and one definition, so `await done === handle.state()` once it has settled (but for an `unpause` after it — see
|
|
303
|
+
* `state`). **BREAKING in 0.9.0 for code that
|
|
304
|
+
* MUTATES `await done`**: it is frozen at every depth, and a write throws in strict code.
|
|
300
305
|
*/
|
|
301
306
|
readonly done: Promise<RunState>;
|
|
302
307
|
/**
|
|
@@ -329,6 +334,33 @@ export interface RunHandle {
|
|
|
329
334
|
* store holds whichever is written first (`RN-R18`).
|
|
330
335
|
*/
|
|
331
336
|
readonly header: CheckpointHeader;
|
|
337
|
+
/**
|
|
338
|
+
* **The run's state NOW — a frozen copy, cached until the next fold input that moves it** — `RN-R44`,
|
|
339
|
+
* [ADR 0122](../../doc/decisions/0122-a-runtimes-live-state-is-a-frozen-copy-on-its-handle.md), P39 (b). Equal to
|
|
340
|
+
* `fold(driver.foldInputs())` at a quiescent point (`RN-R12`: read it on a `queueMicrotask` after an `onFoldInput`, or
|
|
341
|
+
* outside the driver's turn — mid-turn it is a prefix, which the fold is total over and which may read terminal while
|
|
342
|
+
* the run is not). **Frozen at every depth** — a write to it throws in strict code and reaches nothing the runtime keeps
|
|
343
|
+
* — and **the SAME object on every read until an input moves the state**, so reading it twice costs one copy. It is NOT
|
|
344
|
+
* a live view (ADR 0007's hazard): a read is the state at that moment, and the next input makes a new one beside it.
|
|
345
|
+
*
|
|
346
|
+
* **Its cost is O(run) for the first read after each input that moved the state** — a materialisation of every
|
|
347
|
+
* `nodes` entry — so reading it after EVERY input costs O(run²) over a run (P39 prediction 4). Read
|
|
348
|
+
* {@link RunHandle.outcome} and {@link RunHandle.node} per input; read this when you draw the run. On a RESUMED run it
|
|
349
|
+
* holds the stored events too: they are applied before the first delivered input, which `resume()` always delivers
|
|
350
|
+
* before it returns (`RN-R22`). After `done` settles it is the object `done` resolved with — until an `unpause` of a run
|
|
351
|
+
* that finished while paused, the one run control a finished run takes (`RN-R35`): `state()` follows that element and
|
|
352
|
+
* `done` keeps the state at settlement. What `Object.freeze` does not stop — a typed array's elements, an
|
|
353
|
+
* `ArrayBuffer`'s bytes, a `Date`'s time, a `Map`'s or a `Set`'s membership — stays writable through a read and reaches
|
|
354
|
+
* later reads: the stated limit (`RN-R44`). **BREAKING in 0.9.0 for code that BUILDS a `RunHandle`** (P39 (d)) — with `outcome` and `node`.
|
|
355
|
+
*/
|
|
356
|
+
state(): RunState;
|
|
357
|
+
/** **The run's outcome now, in O(1)** — `state().outcome` without the copy (`RN-R44`); the read to make after every input. */
|
|
358
|
+
outcome(): RunOutcome;
|
|
359
|
+
/**
|
|
360
|
+
* **One task's entry now, in O(1) in the run, or `null`** — `state().nodes[taskId]`, the same frozen object, without
|
|
361
|
+
* materialising the rest (`RN-R44`); the first read of an entry freezes it once, O(the entry). `null` for a task the run never dispatched or asked, whatever the id names (`toString` included).
|
|
362
|
+
*/
|
|
363
|
+
node(taskId: string): NodeRun | null;
|
|
332
364
|
}
|
|
333
365
|
export interface Runtime {
|
|
334
366
|
/** **Exactly the object this runtime hands its own drivers** (decision 14) — spread it into `new Driver(…)` beside a `runId`, a `scheduler` and a `joinPolicy` and you have built what `start` builds. */
|