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
|
@@ -3089,6 +3089,49 @@ var defaultCreateTask = (config) => new FyflowTask(config);
|
|
|
3089
3089
|
var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action", "task.cancelled"];
|
|
3090
3090
|
var SUBSCRIBED_EVENTS = [...SETTLEMENT_EVENTS, "task.running"];
|
|
3091
3091
|
var PASS_THROUGH = [["task.message", "onTaskMessage"], ["task.progress", "onTaskProgress"]];
|
|
3092
|
+
var NO_RECORDS = Object.freeze([]);
|
|
3093
|
+
var TaskLog = class {
|
|
3094
|
+
byTask = /* @__PURE__ */ new Map();
|
|
3095
|
+
visits = 0;
|
|
3096
|
+
/** `append`'s half: the record joins its task's sub-log, by reference. */
|
|
3097
|
+
push(record) {
|
|
3098
|
+
const sub = this.byTask.get(record.taskId);
|
|
3099
|
+
if (sub) sub.push(record);
|
|
3100
|
+
else this.byTask.set(record.taskId, [record]);
|
|
3101
|
+
}
|
|
3102
|
+
/** One task's records, in append order — what `effectiveOutcome` and `isSuspended` are handed. Read, never kept. */
|
|
3103
|
+
of(taskId) {
|
|
3104
|
+
const sub = this.byTask.get(taskId) ?? NO_RECORDS;
|
|
3105
|
+
this.visits += sub.length;
|
|
3106
|
+
return sub;
|
|
3107
|
+
}
|
|
3108
|
+
};
|
|
3109
|
+
var Fifo = class {
|
|
3110
|
+
items = [];
|
|
3111
|
+
head = 0;
|
|
3112
|
+
get length() {
|
|
3113
|
+
return this.items.length - this.head;
|
|
3114
|
+
}
|
|
3115
|
+
push(x) {
|
|
3116
|
+
this.items.push(x);
|
|
3117
|
+
}
|
|
3118
|
+
/** The oldest element, removed — `undefined` when empty, as `Array.prototype.shift`. */
|
|
3119
|
+
shift() {
|
|
3120
|
+
if (this.head >= this.items.length) return void 0;
|
|
3121
|
+
const x = this.items[this.head];
|
|
3122
|
+
this.items[this.head] = void 0;
|
|
3123
|
+
this.head += 1;
|
|
3124
|
+
if (this.head === this.items.length) {
|
|
3125
|
+
this.items = [];
|
|
3126
|
+
this.head = 0;
|
|
3127
|
+
}
|
|
3128
|
+
return x;
|
|
3129
|
+
}
|
|
3130
|
+
/** Every element still queued, oldest first. */
|
|
3131
|
+
*[Symbol.iterator]() {
|
|
3132
|
+
for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
3092
3135
|
var Driver = class {
|
|
3093
3136
|
/**
|
|
3094
3137
|
* `OR-R7` — nothing outside the inbound API writes to run state. Private, and
|
|
@@ -3106,13 +3149,21 @@ var Driver = class {
|
|
|
3106
3149
|
* channels with no shared order, so `RN-R3`'s *"read in append order"* was about
|
|
3107
3150
|
* a value no one could hold.
|
|
3108
3151
|
*
|
|
3109
|
-
* `log` above is the **record-kind subsequence of this array**,
|
|
3110
|
-
*
|
|
3111
|
-
*
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3152
|
+
* `log` above is the **record-kind subsequence of this array**, by reference — the
|
|
3153
|
+
* same record objects, never copies; `test/driver/` asserts the two agree, so the
|
|
3154
|
+
* redundancy is checked rather than trusted. *(It was kept "only because
|
|
3155
|
+
* `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
|
|
3156
|
+
* whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
|
|
3157
|
+
* It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
|
|
3158
|
+
* catch-up, and the retention walk's alias check.)*
|
|
3114
3159
|
*/
|
|
3115
3160
|
inputs = [];
|
|
3161
|
+
/**
|
|
3162
|
+
* **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
|
|
3163
|
+
* `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
|
|
3164
|
+
* `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
|
|
3165
|
+
*/
|
|
3166
|
+
taskLog = new TaskLog();
|
|
3116
3167
|
graph;
|
|
3117
3168
|
regions;
|
|
3118
3169
|
dispatched = /* @__PURE__ */ new Set();
|
|
@@ -3360,7 +3411,7 @@ var Driver = class {
|
|
|
3360
3411
|
* brief: *an owner element a resume's replay feeds is TAKEN AT REST*): accepted at the call — every refusal already asked — and its resolution
|
|
3361
3412
|
* appended when the step ends, in the same call order as the pauses beside it.
|
|
3362
3413
|
*/
|
|
3363
|
-
pausePending =
|
|
3414
|
+
pausePending = new Fifo();
|
|
3364
3415
|
/**
|
|
3365
3416
|
* **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
|
|
3366
3417
|
* number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
|
|
@@ -3375,7 +3426,7 @@ var Driver = class {
|
|
|
3375
3426
|
* every one is appended when the step ends, BEFORE a pause, an unpause, an answer or a stop pending on that step is taken
|
|
3376
3427
|
* (`leaveStep()`): the attempt started before any of them. Empty at rest.
|
|
3377
3428
|
*/
|
|
3378
|
-
attemptsPending =
|
|
3429
|
+
attemptsPending = new Fifo();
|
|
3379
3430
|
/**
|
|
3380
3431
|
* **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
|
|
3381
3432
|
* `RN-R29`,
|
|
@@ -3874,9 +3925,7 @@ var Driver = class {
|
|
|
3874
3925
|
* handing it to a worker again would run what a person was asked to decide.
|
|
3875
3926
|
*/
|
|
3876
3927
|
inFlight() {
|
|
3877
|
-
|
|
3878
|
-
for (const r of this.log) if (r.kind === "outcome") settled.add(r.taskId);
|
|
3879
|
-
return [...this.dispatched].filter((id) => !settled.has(id));
|
|
3928
|
+
return [...this.dispatched].filter((id) => !this.taskLog.of(id).some((r) => r.kind === "outcome"));
|
|
3880
3929
|
}
|
|
3881
3930
|
/**
|
|
3882
3931
|
* **Whether a callback fires for the element at `seq`** — `RN-R24`. Always, except during a replay,
|
|
@@ -3912,7 +3961,7 @@ var Driver = class {
|
|
|
3912
3961
|
/** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3913
3962
|
resolveStep(taskId, resolvedAs, value, midStep = false) {
|
|
3914
3963
|
if (!this.parked(taskId)) return false;
|
|
3915
|
-
|
|
3964
|
+
for (const c of this.pausePending) if (c.kind === "answer" && c.record.taskId === taskId) return false;
|
|
3916
3965
|
const { runId, nodeId, branchPath } = parseTaskId(taskId);
|
|
3917
3966
|
if (this.shut() || this.stopRequest !== null) return false;
|
|
3918
3967
|
if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
|
|
@@ -4267,7 +4316,7 @@ var Driver = class {
|
|
|
4267
4316
|
throw new RunControlRefused(
|
|
4268
4317
|
"pause",
|
|
4269
4318
|
"paused",
|
|
4270
|
-
`RunControlRefused (paused): run "${this.runId}" is already paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 by a call earlier in this step, taken when it ends" : ""} (RN-R35, P34 (e)). Unpause it first. Nothing was recorded.`
|
|
4319
|
+
`RunControlRefused (paused): run "${this.runId}" is already paused${[...this.pausePending].some((c) => c.kind !== "answer") ? " \u2014 by a call earlier in this step, taken when it ends" : ""} (RN-R35, P34 (e)). Unpause it first. Nothing was recorded.`
|
|
4271
4320
|
);
|
|
4272
4321
|
}
|
|
4273
4322
|
if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
|
|
@@ -4293,7 +4342,7 @@ var Driver = class {
|
|
|
4293
4342
|
throw new RunControlRefused(
|
|
4294
4343
|
"unpause",
|
|
4295
4344
|
"not-paused",
|
|
4296
|
-
`RunControlRefused (not-paused): run "${this.runId}" is not paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 an unpause earlier in this step already lifts it" : ""} (RN-R35, P34 (e)). Nothing was recorded and nothing was handed on.`
|
|
4345
|
+
`RunControlRefused (not-paused): run "${this.runId}" is not paused${[...this.pausePending].some((c) => c.kind !== "answer") ? " \u2014 an unpause earlier in this step already lifts it" : ""} (RN-R35, P34 (e)). Nothing was recorded and nothing was handed on.`
|
|
4297
4346
|
);
|
|
4298
4347
|
}
|
|
4299
4348
|
this.pausePending.push({ kind: "unpaused", actor: options.actor });
|
|
@@ -4301,7 +4350,7 @@ var Driver = class {
|
|
|
4301
4350
|
}
|
|
4302
4351
|
/** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
|
|
4303
4352
|
pauseWanted() {
|
|
4304
|
-
const last = this.pausePending.filter((c) => c.kind !== "answer").at(-1);
|
|
4353
|
+
const last = [...this.pausePending].filter((c) => c.kind !== "answer").at(-1);
|
|
4305
4354
|
return last !== void 0 ? last.kind === "paused" : this.paused;
|
|
4306
4355
|
}
|
|
4307
4356
|
/**
|
|
@@ -4694,7 +4743,7 @@ var Driver = class {
|
|
|
4694
4743
|
if (!node) return;
|
|
4695
4744
|
const itemIndices = this.indicesFrom(nodeId, branchPath);
|
|
4696
4745
|
const taskId = buildTaskId(runId, nodeId, branchIndices(this.pathOf(nodeId, itemIndices)));
|
|
4697
|
-
const outcome = effectiveOutcome(this.
|
|
4746
|
+
const outcome = effectiveOutcome(this.taskLog.of(taskId), taskId);
|
|
4698
4747
|
if (!outcome) return;
|
|
4699
4748
|
const result = selectEdge(
|
|
4700
4749
|
node,
|
|
@@ -5041,7 +5090,7 @@ var Driver = class {
|
|
|
5041
5090
|
* [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)
|
|
5042
5091
|
* rules 2 and 3, and `FD-C2`.
|
|
5043
5092
|
*
|
|
5044
|
-
* **It is a PROJECTION of the log and adds no retention** — measured at
|
|
5093
|
+
* **It is a PROJECTION of the log and adds no retention of a result** — measured at
|
|
5045
5094
|
* [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
|
|
5046
5095
|
* declaration and again before this was written: this driver holds
|
|
5047
5096
|
* `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
|
|
@@ -5051,14 +5100,11 @@ var Driver = class {
|
|
|
5051
5100
|
* described **today** before this method existed, and this adds **no retention**.
|
|
5052
5101
|
* *(Q19 is RESOLVED since W53 —
|
|
5053
5102
|
* [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
|
|
5054
|
-
* — and `RN-R13` names
|
|
5103
|
+
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
|
|
5055
5104
|
* asserted by IDENTITY at ladder L2 rather than by a byte count, because a
|
|
5056
5105
|
* reachability SET cannot tell an alias from a copy. W53's readers found that
|
|
5057
5106
|
* from both ends — the rule's stated test returned a third name on a `needs`
|
|
5058
5107
|
* flow, and cloning here cost ZERO.)*
|
|
5059
|
-
* *(It adds ONE field — `outputsCache` below — which is a memo the log invalidates
|
|
5060
|
-
* and which holds nothing the log does not. This read *"zero fields"* until W41's
|
|
5061
|
-
* conformity reader read it beside the memo the cost measurement had just added.)*
|
|
5062
5108
|
*
|
|
5063
5109
|
* **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
|
|
5064
5110
|
* section 3 rule 4's own reason: a resumed node has two records in the log and
|
|
@@ -5066,43 +5112,97 @@ var Driver = class {
|
|
|
5066
5112
|
* suspended has no effective outcome and therefore no entry — which is what makes
|
|
5067
5113
|
* `FD-R3` fire rather than a reference quietly resolving to `undefined`.
|
|
5068
5114
|
*
|
|
5069
|
-
* **
|
|
5070
|
-
*
|
|
5071
|
-
*
|
|
5072
|
-
*
|
|
5073
|
-
* `
|
|
5074
|
-
*
|
|
5115
|
+
* **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
|
|
5116
|
+
* [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
|
|
5117
|
+
* rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
|
|
5118
|
+
* WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
|
|
5119
|
+
* of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
|
|
5120
|
+
* construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
|
|
5121
|
+
* `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
|
|
5122
|
+
* 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
|
|
5123
|
+
* the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
|
|
5124
|
+
* (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
|
|
5125
|
+
* in its tasks' first-record order, each `result` the log record's own object.
|
|
5126
|
+
*
|
|
5127
|
+
* **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
|
|
5128
|
+
* node or an edge that declares a `needs` asks, and a run that declares none never builds it.
|
|
5075
5129
|
*/
|
|
5076
5130
|
outputs() {
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
} catch {
|
|
5092
|
-
continue;
|
|
5131
|
+
const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map() };
|
|
5132
|
+
for (; c.at < this.log.length; c.at += 1) {
|
|
5133
|
+
const r = this.log[c.at];
|
|
5134
|
+
let t = c.tasks.get(r.taskId);
|
|
5135
|
+
if (t === void 0) {
|
|
5136
|
+
let parts;
|
|
5137
|
+
try {
|
|
5138
|
+
parts = parseTaskId(r.taskId);
|
|
5139
|
+
} catch {
|
|
5140
|
+
c.tasks.set(r.taskId, null);
|
|
5141
|
+
continue;
|
|
5142
|
+
}
|
|
5143
|
+
t = { nodeId: parts.nodeId, path: parts.branchPath, order: c.tasks.size, entry: null };
|
|
5144
|
+
c.tasks.set(r.taskId, t);
|
|
5093
5145
|
}
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
else byNode.set(parts.nodeId, [{ path: parts.branchPath, result: eff.result }]);
|
|
5146
|
+
if (t === null) continue;
|
|
5147
|
+
this.place(c, t, effectiveOutcome(this.taskLog.of(r.taskId), r.taskId));
|
|
5097
5148
|
}
|
|
5098
|
-
|
|
5099
|
-
return byNode;
|
|
5149
|
+
return c.byNode;
|
|
5100
5150
|
}
|
|
5101
5151
|
/**
|
|
5102
|
-
*
|
|
5103
|
-
*
|
|
5104
|
-
*
|
|
5105
|
-
*
|
|
5152
|
+
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
5153
|
+
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
5154
|
+
* in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
|
|
5155
|
+
*
|
|
5156
|
+
* **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
|
|
5157
|
+
* RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
|
|
5158
|
+
* at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
|
|
5159
|
+
* a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
|
|
5160
|
+
* `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
|
|
5161
|
+
* fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
|
|
5162
|
+
* that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
|
|
5163
|
+
* so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
|
|
5164
|
+
* none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
|
|
5165
|
+
*/
|
|
5166
|
+
place(c, t, eff) {
|
|
5167
|
+
const list = c.byNode.get(t.nodeId);
|
|
5168
|
+
const orders = c.orders.get(t.nodeId);
|
|
5169
|
+
if (eff === null) {
|
|
5170
|
+
if (t.entry === null || list === void 0 || orders === void 0) return;
|
|
5171
|
+
const at = list.indexOf(t.entry);
|
|
5172
|
+
list.splice(at, 1);
|
|
5173
|
+
orders.splice(at, 1);
|
|
5174
|
+
t.entry = null;
|
|
5175
|
+
if (list.length === 0) {
|
|
5176
|
+
c.byNode.delete(t.nodeId);
|
|
5177
|
+
c.orders.delete(t.nodeId);
|
|
5178
|
+
}
|
|
5179
|
+
return;
|
|
5180
|
+
}
|
|
5181
|
+
const entry = { path: t.path, result: eff.result };
|
|
5182
|
+
if (t.entry !== null && list !== void 0) {
|
|
5183
|
+
list[list.indexOf(t.entry)] = entry;
|
|
5184
|
+
t.entry = entry;
|
|
5185
|
+
return;
|
|
5186
|
+
}
|
|
5187
|
+
t.entry = entry;
|
|
5188
|
+
if (list === void 0 || orders === void 0) {
|
|
5189
|
+
c.byNode.set(t.nodeId, [entry]);
|
|
5190
|
+
c.orders.set(t.nodeId, [t.order]);
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
5193
|
+
let i = list.length;
|
|
5194
|
+
while (i > 0 && orders[i - 1] > t.order) i -= 1;
|
|
5195
|
+
list.splice(i, 0, entry);
|
|
5196
|
+
orders.splice(i, 0, t.order);
|
|
5197
|
+
}
|
|
5198
|
+
/**
|
|
5199
|
+
* The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
|
|
5200
|
+
* it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
|
|
5201
|
+
* never a copy — `tasks` is one slot per task id
|
|
5202
|
+
* read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
|
|
5203
|
+
* node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
|
|
5204
|
+
* read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
|
|
5205
|
+
* *the driver computes no run state* is untouched. `null` until a `needs` first asks.
|
|
5106
5206
|
*/
|
|
5107
5207
|
outputsCache = null;
|
|
5108
5208
|
/**
|
|
@@ -5332,7 +5432,7 @@ var Driver = class {
|
|
|
5332
5432
|
* predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
|
|
5333
5433
|
*/
|
|
5334
5434
|
parked(taskId) {
|
|
5335
|
-
return isSuspended(this.
|
|
5435
|
+
return isSuspended(this.taskLog.of(taskId), taskId, this.asked.has(taskId));
|
|
5336
5436
|
}
|
|
5337
5437
|
/**
|
|
5338
5438
|
* **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
|
|
@@ -5341,7 +5441,7 @@ var Driver = class {
|
|
|
5341
5441
|
* never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
|
|
5342
5442
|
*/
|
|
5343
5443
|
hasParked() {
|
|
5344
|
-
for (const id of this.dispatched) if (isSuspended(this.
|
|
5444
|
+
for (const id of this.dispatched) if (isSuspended(this.taskLog.of(id), id)) return true;
|
|
5345
5445
|
for (const id of this.asked) if (this.parked(id)) return true;
|
|
5346
5446
|
return false;
|
|
5347
5447
|
}
|
|
@@ -5619,13 +5719,18 @@ var Driver = class {
|
|
|
5619
5719
|
*
|
|
5620
5720
|
* A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
|
|
5621
5721
|
* the stored object and `OR-R8` derives rather than appends, so a second copy
|
|
5622
|
-
* here would be a second truth.
|
|
5722
|
+
* here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
|
|
5723
|
+
* by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
|
|
5724
|
+
* element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
|
|
5623
5725
|
* copy, frozen, for the same reason `snapshot()` is: a listener holding the
|
|
5624
5726
|
* driver's own array could forge a settlement and bypass `RS-R1`.
|
|
5625
5727
|
*/
|
|
5626
5728
|
append(input) {
|
|
5627
5729
|
const seq = this.inputs.push(input) - 1;
|
|
5628
|
-
if (input.kind === "outcome" || input.kind === "resolution")
|
|
5730
|
+
if (input.kind === "outcome" || input.kind === "resolution") {
|
|
5731
|
+
this.log.push(input.record);
|
|
5732
|
+
this.taskLog.push(input.record);
|
|
5733
|
+
}
|
|
5629
5734
|
const replay = this.replay;
|
|
5630
5735
|
if (replay !== null && seq < replay.stored.length) {
|
|
5631
5736
|
const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
|
|
@@ -5850,7 +5955,7 @@ var Driver = class {
|
|
|
5850
5955
|
* the way in.
|
|
5851
5956
|
*/
|
|
5852
5957
|
effectiveOutcomeOf(taskId) {
|
|
5853
|
-
const o = effectiveOutcome(this.
|
|
5958
|
+
const o = effectiveOutcome(this.taskLog.of(taskId), taskId);
|
|
5854
5959
|
return o === null ? null : cloneRecord(o);
|
|
5855
5960
|
}
|
|
5856
5961
|
terminalStateOf(taskId) {
|
|
@@ -5858,6 +5963,484 @@ var Driver = class {
|
|
|
5858
5963
|
}
|
|
5859
5964
|
};
|
|
5860
5965
|
|
|
5966
|
+
// src/fold/accumulator.ts
|
|
5967
|
+
var Suspended = class {
|
|
5968
|
+
items = [];
|
|
5969
|
+
alive = /* @__PURE__ */ new Map();
|
|
5970
|
+
gen = 0;
|
|
5971
|
+
count;
|
|
5972
|
+
constructor(from, count) {
|
|
5973
|
+
this.count = count;
|
|
5974
|
+
const g = ++this.gen;
|
|
5975
|
+
for (const v of from) {
|
|
5976
|
+
this.alive.set(v, g);
|
|
5977
|
+
this.items.push({ v, g });
|
|
5978
|
+
}
|
|
5979
|
+
count(from.length);
|
|
5980
|
+
}
|
|
5981
|
+
has(v) {
|
|
5982
|
+
return this.alive.has(v);
|
|
5983
|
+
}
|
|
5984
|
+
add(v) {
|
|
5985
|
+
if (this.alive.has(v)) return;
|
|
5986
|
+
const g = ++this.gen;
|
|
5987
|
+
this.alive.set(v, g);
|
|
5988
|
+
this.items.push({ v, g });
|
|
5989
|
+
}
|
|
5990
|
+
remove(v) {
|
|
5991
|
+
this.alive.delete(v);
|
|
5992
|
+
}
|
|
5993
|
+
toArray() {
|
|
5994
|
+
this.count(this.items.length);
|
|
5995
|
+
const out = [];
|
|
5996
|
+
for (const e of this.items) if (this.alive.get(e.v) === e.g) out.push(e.v);
|
|
5997
|
+
return out;
|
|
5998
|
+
}
|
|
5999
|
+
};
|
|
6000
|
+
function regionWeight(opened, joined) {
|
|
6001
|
+
const unjoined = joined !== true ? 1 : 0;
|
|
6002
|
+
return { unjoined, openedNothing: opened.length === 0 && joined !== true ? 1 : 0 };
|
|
6003
|
+
}
|
|
6004
|
+
var hasOwn2 = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
6005
|
+
function put(o, k, v) {
|
|
6006
|
+
if (k === "__proto__") Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
|
|
6007
|
+
else o[k] = v;
|
|
6008
|
+
}
|
|
6009
|
+
var Accumulator = class _Accumulator {
|
|
6010
|
+
runId;
|
|
6011
|
+
outcome;
|
|
6012
|
+
haltedBy;
|
|
6013
|
+
cancelRequest;
|
|
6014
|
+
interruption;
|
|
6015
|
+
paused;
|
|
6016
|
+
/** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
|
|
6017
|
+
nodes = /* @__PURE__ */ new Map();
|
|
6018
|
+
/** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
|
|
6019
|
+
regions = /* @__PURE__ */ new Map();
|
|
6020
|
+
pathEnds = [];
|
|
6021
|
+
resumes = [];
|
|
6022
|
+
gated = [];
|
|
6023
|
+
counts = null;
|
|
6024
|
+
base;
|
|
6025
|
+
counter;
|
|
6026
|
+
constructor(base, counter) {
|
|
6027
|
+
this.base = base;
|
|
6028
|
+
this.counter = counter;
|
|
6029
|
+
this.runId = base.runId;
|
|
6030
|
+
this.outcome = base.outcome;
|
|
6031
|
+
this.haltedBy = base.haltedBy;
|
|
6032
|
+
this.cancelRequest = base.cancelRequest;
|
|
6033
|
+
this.interruption = base.interruption;
|
|
6034
|
+
this.paused = base.paused;
|
|
6035
|
+
}
|
|
6036
|
+
/** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
|
|
6037
|
+
static load(state, counter) {
|
|
6038
|
+
return new _Accumulator(state, counter);
|
|
6039
|
+
}
|
|
6040
|
+
tick(n = 1) {
|
|
6041
|
+
if (this.counter !== void 0) this.counter.visits += n;
|
|
6042
|
+
}
|
|
6043
|
+
// --- the reads a runtime keeps an accumulator OPEN for — W196, ADR 0122, `RN-R44` -----------------------------
|
|
6044
|
+
/**
|
|
6045
|
+
* **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
|
|
6046
|
+
* O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
|
|
6047
|
+
*/
|
|
6048
|
+
outcomeNow() {
|
|
6049
|
+
return this.outcome;
|
|
6050
|
+
}
|
|
6051
|
+
/**
|
|
6052
|
+
* **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
|
|
6053
|
+
* An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
|
|
6054
|
+
* dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
|
|
6055
|
+
*/
|
|
6056
|
+
entry(taskId) {
|
|
6057
|
+
if (this.nodes.has(taskId)) return this.nodes.get(taskId);
|
|
6058
|
+
return hasOwn2(this.base.nodes, taskId) ? this.base.nodes[taskId] : void 0;
|
|
6059
|
+
}
|
|
6060
|
+
// --- reads, through to the base ---------------------------------------------------------------------------
|
|
6061
|
+
/** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
|
|
6062
|
+
node(taskId) {
|
|
6063
|
+
return this.nodes.has(taskId) ? this.nodes.get(taskId) : this.base.nodes[taskId];
|
|
6064
|
+
}
|
|
6065
|
+
/** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
|
|
6066
|
+
instance(regionId, regionPath) {
|
|
6067
|
+
const touched = this.regions.get(regionId)?.get(regionPath);
|
|
6068
|
+
return touched ?? this.base.regions[regionId]?.[regionPath];
|
|
6069
|
+
}
|
|
6070
|
+
/** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
|
|
6071
|
+
tally() {
|
|
6072
|
+
if (this.counts !== null) return this.counts;
|
|
6073
|
+
const c = { byState: /* @__PURE__ */ new Map(), nodes: 0, unjoined: 0, openedNothing: 0, noEligibleEdge: 0, ends: 0, pathEnds: 0 };
|
|
6074
|
+
const baseNodes = this.base.nodes;
|
|
6075
|
+
const keys = Object.keys(baseNodes);
|
|
6076
|
+
this.tick(keys.length);
|
|
6077
|
+
for (const k of keys) if (!this.nodes.has(k)) this.countNode(c, baseNodes[k], 1);
|
|
6078
|
+
for (const n of this.nodes.values()) this.countNode(c, n, 1);
|
|
6079
|
+
this.tick(this.nodes.size);
|
|
6080
|
+
const baseRegions = this.base.regions;
|
|
6081
|
+
for (const id of Object.keys(baseRegions)) {
|
|
6082
|
+
const byPath = baseRegions[id];
|
|
6083
|
+
const touched = this.regions.get(id);
|
|
6084
|
+
for (const p of Object.keys(byPath)) {
|
|
6085
|
+
this.tick();
|
|
6086
|
+
if (touched?.has(p)) continue;
|
|
6087
|
+
const r = byPath[p];
|
|
6088
|
+
const w = regionWeight(r.opened, r.joined);
|
|
6089
|
+
c.unjoined += w.unjoined;
|
|
6090
|
+
c.openedNothing += w.openedNothing;
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
for (const byPath of this.regions.values()) {
|
|
6094
|
+
for (const acc of byPath.values()) {
|
|
6095
|
+
this.tick();
|
|
6096
|
+
const w = regionWeight(acc.opened, acc.joined);
|
|
6097
|
+
c.unjoined += w.unjoined;
|
|
6098
|
+
c.openedNothing += w.openedNothing;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
for (const list of [this.base.pathEnds, this.pathEnds]) {
|
|
6102
|
+
this.tick(list.length);
|
|
6103
|
+
for (const e of list) {
|
|
6104
|
+
c.pathEnds += 1;
|
|
6105
|
+
if (e.reason === "no-eligible-edge") c.noEligibleEdge += 1;
|
|
6106
|
+
if (e.reason === "end") c.ends += 1;
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
this.counts = c;
|
|
6110
|
+
return c;
|
|
6111
|
+
}
|
|
6112
|
+
countNode(c, n, by) {
|
|
6113
|
+
c.nodes += by;
|
|
6114
|
+
c.byState.set(n.state, (c.byState.get(n.state) ?? 0) + by);
|
|
6115
|
+
}
|
|
6116
|
+
stateCount(state) {
|
|
6117
|
+
return this.tally().byState.get(state) ?? 0;
|
|
6118
|
+
}
|
|
6119
|
+
// --- writes ------------------------------------------------------------------------------------------------
|
|
6120
|
+
/** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
|
|
6121
|
+
setNode(taskId, entry) {
|
|
6122
|
+
if (this.counts !== null) {
|
|
6123
|
+
const own3 = this.nodes.has(taskId) || hasOwn2(this.base.nodes, taskId);
|
|
6124
|
+
if (own3) this.countNode(this.counts, this.node(taskId), -1);
|
|
6125
|
+
this.countNode(this.counts, entry, 1);
|
|
6126
|
+
}
|
|
6127
|
+
this.nodes.set(taskId, entry);
|
|
6128
|
+
this.tick();
|
|
6129
|
+
}
|
|
6130
|
+
/**
|
|
6131
|
+
* The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
|
|
6132
|
+
* paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
|
|
6133
|
+
*/
|
|
6134
|
+
touchRegion(regionId, regionPath) {
|
|
6135
|
+
let byPath = this.regions.get(regionId);
|
|
6136
|
+
let acc = byPath?.get(regionPath);
|
|
6137
|
+
if (acc === void 0) {
|
|
6138
|
+
const at = this.base.regions[regionId]?.[regionPath];
|
|
6139
|
+
if (at) {
|
|
6140
|
+
this.tick(at.opened.length + Object.keys(at.settled).length);
|
|
6141
|
+
acc = {
|
|
6142
|
+
origin: at,
|
|
6143
|
+
opened: [...at.opened],
|
|
6144
|
+
openedSet: new Set(at.opened),
|
|
6145
|
+
settled: new Map(Object.entries(at.settled)),
|
|
6146
|
+
suspended: new Suspended(at.suspended, (n) => this.tick(n)),
|
|
6147
|
+
joined: at.joined
|
|
6148
|
+
};
|
|
6149
|
+
} else {
|
|
6150
|
+
acc = {
|
|
6151
|
+
origin: { opened: [], settled: {}, suspended: [], joined: null },
|
|
6152
|
+
opened: [],
|
|
6153
|
+
openedSet: /* @__PURE__ */ new Set(),
|
|
6154
|
+
settled: /* @__PURE__ */ new Map(),
|
|
6155
|
+
suspended: new Suspended([], (n) => this.tick(n)),
|
|
6156
|
+
joined: null
|
|
6157
|
+
};
|
|
6158
|
+
}
|
|
6159
|
+
if (byPath === void 0) {
|
|
6160
|
+
byPath = /* @__PURE__ */ new Map();
|
|
6161
|
+
this.regions.set(regionId, byPath);
|
|
6162
|
+
}
|
|
6163
|
+
byPath.set(regionPath, acc);
|
|
6164
|
+
const inner = this.base.regions[regionId];
|
|
6165
|
+
if (this.counts !== null && at && inner !== void 0 && hasOwn2(this.base.regions, regionId) && hasOwn2(inner, regionPath)) {
|
|
6166
|
+
this.unweigh(regionWeight(at.opened, at.joined));
|
|
6167
|
+
}
|
|
6168
|
+
} else if (this.counts !== null) {
|
|
6169
|
+
this.unweigh(regionWeight(acc.opened, acc.joined));
|
|
6170
|
+
}
|
|
6171
|
+
this.tick();
|
|
6172
|
+
return acc;
|
|
6173
|
+
}
|
|
6174
|
+
unweigh(w) {
|
|
6175
|
+
this.counts.unjoined -= w.unjoined;
|
|
6176
|
+
this.counts.openedNothing -= w.openedNothing;
|
|
6177
|
+
}
|
|
6178
|
+
/** The instance's share of the region counts put back after a write. */
|
|
6179
|
+
settleRegion(acc) {
|
|
6180
|
+
if (this.counts === null) return;
|
|
6181
|
+
const w = regionWeight(acc.opened, acc.joined);
|
|
6182
|
+
this.counts.unjoined += w.unjoined;
|
|
6183
|
+
this.counts.openedNothing += w.openedNothing;
|
|
6184
|
+
}
|
|
6185
|
+
addPathEnd(e) {
|
|
6186
|
+
this.pathEnds.push(e);
|
|
6187
|
+
this.tick();
|
|
6188
|
+
if (this.counts !== null) {
|
|
6189
|
+
this.counts.pathEnds += 1;
|
|
6190
|
+
if (e.reason === "no-eligible-edge") this.counts.noEligibleEdge += 1;
|
|
6191
|
+
if (e.reason === "end") this.counts.ends += 1;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
// --- the arms -------------------------------------------------------------------------------------------------
|
|
6195
|
+
/**
|
|
6196
|
+
* **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
|
|
6197
|
+
* `false` where it returned the state it was handed, so `step` can return that same object. Throws where
|
|
6198
|
+
* that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
|
|
6199
|
+
* the module header's stated limit on region paths.
|
|
6200
|
+
*/
|
|
6201
|
+
apply(input) {
|
|
6202
|
+
this.tick();
|
|
6203
|
+
if (input.kind === "halted" || input.kind === "stopped") {
|
|
6204
|
+
this.haltedBy = input.reason;
|
|
6205
|
+
this.outcome = "halted";
|
|
6206
|
+
return true;
|
|
6207
|
+
}
|
|
6208
|
+
if (input.kind === "cancelRequested") {
|
|
6209
|
+
if (this.cancelRequest !== null || this.outcome === "cancelled") return false;
|
|
6210
|
+
this.cancelRequest = { actor: input.actor, reason: input.reason };
|
|
6211
|
+
return this.withOutcome();
|
|
6212
|
+
}
|
|
6213
|
+
if (input.kind === "interrupted") {
|
|
6214
|
+
if (this.interruption !== null) return false;
|
|
6215
|
+
this.interruption = { actor: input.actor, reason: input.reason };
|
|
6216
|
+
return this.withOutcome();
|
|
6217
|
+
}
|
|
6218
|
+
if (input.kind === "paused" || input.kind === "unpaused") {
|
|
6219
|
+
const paused = input.kind === "paused";
|
|
6220
|
+
if (this.paused === paused) return false;
|
|
6221
|
+
this.paused = paused;
|
|
6222
|
+
return true;
|
|
6223
|
+
}
|
|
6224
|
+
if (input.kind === "region") {
|
|
6225
|
+
this.applyRegion(input.event);
|
|
6226
|
+
return this.withOutcome();
|
|
6227
|
+
}
|
|
6228
|
+
if (input.kind === "dispatched") {
|
|
6229
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
6230
|
+
if (this.node(input.taskId)) return false;
|
|
6231
|
+
this.runId = this.runId ?? runId2;
|
|
6232
|
+
this.setNode(input.taskId, {
|
|
6233
|
+
runId: runId2,
|
|
6234
|
+
nodeId: nodeId2,
|
|
6235
|
+
branchPath: branchPath2,
|
|
6236
|
+
flowAttempt: attempt2,
|
|
6237
|
+
attemptNumber: null,
|
|
6238
|
+
state: "running",
|
|
6239
|
+
result: null,
|
|
6240
|
+
error: null
|
|
6241
|
+
});
|
|
6242
|
+
return this.withOutcome();
|
|
6243
|
+
}
|
|
6244
|
+
if (input.kind === "asked") {
|
|
6245
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
6246
|
+
if (this.node(input.taskId)) return false;
|
|
6247
|
+
this.runId = this.runId ?? runId2;
|
|
6248
|
+
this.setNode(input.taskId, {
|
|
6249
|
+
runId: runId2,
|
|
6250
|
+
nodeId: nodeId2,
|
|
6251
|
+
branchPath: branchPath2,
|
|
6252
|
+
flowAttempt: attempt2,
|
|
6253
|
+
attemptNumber: null,
|
|
6254
|
+
state: "suspended",
|
|
6255
|
+
result: null,
|
|
6256
|
+
error: null,
|
|
6257
|
+
input: structuredClone(input.input)
|
|
6258
|
+
});
|
|
6259
|
+
return this.withOutcome();
|
|
6260
|
+
}
|
|
6261
|
+
if (input.kind === "pathEnded") {
|
|
6262
|
+
this.addPathEnd({ reason: input.reason, at: input.at });
|
|
6263
|
+
return this.withOutcome();
|
|
6264
|
+
}
|
|
6265
|
+
if (input.kind === "regionEntered") {
|
|
6266
|
+
if (this.instance(input.regionId, input.regionPath)) return false;
|
|
6267
|
+
const acc = this.touchRegion(input.regionId, input.regionPath);
|
|
6268
|
+
this.settleRegion(acc);
|
|
6269
|
+
return this.withOutcome();
|
|
6270
|
+
}
|
|
6271
|
+
if (input.kind === "resumed") {
|
|
6272
|
+
this.resumes.push([...input.taskIds]);
|
|
6273
|
+
this.tick(input.taskIds.length + 1);
|
|
6274
|
+
if (this.interruption !== null) {
|
|
6275
|
+
this.interruption = null;
|
|
6276
|
+
return this.withOutcome();
|
|
6277
|
+
}
|
|
6278
|
+
return true;
|
|
6279
|
+
}
|
|
6280
|
+
if (input.kind === "gated") {
|
|
6281
|
+
const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
|
|
6282
|
+
if (input.key !== void 0) d.key = structuredClone(input.key);
|
|
6283
|
+
this.gated.push(d);
|
|
6284
|
+
this.tick();
|
|
6285
|
+
return true;
|
|
6286
|
+
}
|
|
6287
|
+
if (input.kind === "attempted") {
|
|
6288
|
+
const node = this.node(input.taskId);
|
|
6289
|
+
if (node === void 0) return false;
|
|
6290
|
+
this.setNode(input.taskId, { ...node, attemptNumber: input.attemptNumber });
|
|
6291
|
+
return true;
|
|
6292
|
+
}
|
|
6293
|
+
if (input.kind === "joinSettled") {
|
|
6294
|
+
const acc = this.touchRegion(input.regionId, input.regionPath);
|
|
6295
|
+
acc.joined = input.fired;
|
|
6296
|
+
this.settleRegion(acc);
|
|
6297
|
+
return this.withOutcome();
|
|
6298
|
+
}
|
|
6299
|
+
const r = input.record;
|
|
6300
|
+
const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
|
|
6301
|
+
const existing = this.node(r.taskId);
|
|
6302
|
+
const attemptNumber = existing?.attemptNumber ?? null;
|
|
6303
|
+
if (r.kind === "outcome") {
|
|
6304
|
+
this.setNode(r.taskId, {
|
|
6305
|
+
runId,
|
|
6306
|
+
nodeId,
|
|
6307
|
+
branchPath,
|
|
6308
|
+
flowAttempt: attempt,
|
|
6309
|
+
attemptNumber,
|
|
6310
|
+
// `user_action` is `suspended` at flow level: a resolution resumes it.
|
|
6311
|
+
state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
|
|
6312
|
+
result: r.result,
|
|
6313
|
+
error: r.error
|
|
6314
|
+
});
|
|
6315
|
+
} else {
|
|
6316
|
+
this.setNode(r.taskId, {
|
|
6317
|
+
runId: existing?.runId ?? runId,
|
|
6318
|
+
nodeId: existing?.nodeId ?? nodeId,
|
|
6319
|
+
branchPath: existing?.branchPath ?? branchPath,
|
|
6320
|
+
flowAttempt: existing?.flowAttempt ?? attempt,
|
|
6321
|
+
attemptNumber,
|
|
6322
|
+
state: r.resolvedAs,
|
|
6323
|
+
result: r.result,
|
|
6324
|
+
error: r.error,
|
|
6325
|
+
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
6326
|
+
});
|
|
6327
|
+
}
|
|
6328
|
+
this.runId = this.runId ?? runId;
|
|
6329
|
+
return this.withOutcome();
|
|
6330
|
+
}
|
|
6331
|
+
applyRegion(e) {
|
|
6332
|
+
const acc = this.touchRegion(e.regionId, e.regionPath);
|
|
6333
|
+
switch (e.type) {
|
|
6334
|
+
case "branch.opened":
|
|
6335
|
+
if (!acc.openedSet.has(e.branchPath)) {
|
|
6336
|
+
acc.opened.push(e.branchPath);
|
|
6337
|
+
acc.openedSet.add(e.branchPath);
|
|
6338
|
+
}
|
|
6339
|
+
break;
|
|
6340
|
+
case "branch.arrived":
|
|
6341
|
+
case "branch.exited":
|
|
6342
|
+
case "branch.lost": {
|
|
6343
|
+
const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
|
|
6344
|
+
acc.settled.set(e.branchPath, how);
|
|
6345
|
+
acc.suspended.remove(e.branchPath);
|
|
6346
|
+
break;
|
|
6347
|
+
}
|
|
6348
|
+
case "branch.suspended":
|
|
6349
|
+
acc.suspended.add(e.branchPath);
|
|
6350
|
+
break;
|
|
6351
|
+
case "branch.resumed":
|
|
6352
|
+
acc.suspended.remove(e.branchPath);
|
|
6353
|
+
break;
|
|
6354
|
+
}
|
|
6355
|
+
this.settleRegion(acc);
|
|
6356
|
+
}
|
|
6357
|
+
/**
|
|
6358
|
+
* **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
|
|
6359
|
+
* one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
|
|
6360
|
+
* recomputes has moved the state.
|
|
6361
|
+
*/
|
|
6362
|
+
withOutcome() {
|
|
6363
|
+
this.outcome = this.precedence();
|
|
6364
|
+
return true;
|
|
6365
|
+
}
|
|
6366
|
+
precedence() {
|
|
6367
|
+
if (this.haltedBy !== null) return "halted";
|
|
6368
|
+
if (this.cancelRequest === null && this.stateCount("cancelled") > 0) return "cancelled";
|
|
6369
|
+
if (this.cancelRequest !== null) return this.stateCount("running") > 0 ? "cancelling" : "cancelled";
|
|
6370
|
+
if (this.interruption !== null) return "interrupted";
|
|
6371
|
+
if (this.stateCount("suspended") > 0) return "awaiting_action";
|
|
6372
|
+
if (this.stateCount("running") > 0) return "running";
|
|
6373
|
+
const c = this.tally();
|
|
6374
|
+
if (c.nodes === 0 && c.pathEnds === 0 && c.openedNothing === 0) return "running";
|
|
6375
|
+
if (c.unjoined > 0) return "stuck";
|
|
6376
|
+
if (c.noEligibleEdge > 0) return "failed";
|
|
6377
|
+
if (c.ends > 0) return "completed";
|
|
6378
|
+
if (this.stateCount("failed") > 0) return "failed";
|
|
6379
|
+
return "completed";
|
|
6380
|
+
}
|
|
6381
|
+
// --- materialisation ------------------------------------------------------------------------------------------
|
|
6382
|
+
/**
|
|
6383
|
+
* **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
|
|
6384
|
+
* 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
|
|
6385
|
+
* entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
|
|
6386
|
+
*/
|
|
6387
|
+
materialise() {
|
|
6388
|
+
const b = this.base;
|
|
6389
|
+
let nodes = b.nodes;
|
|
6390
|
+
if (this.nodes.size > 0) {
|
|
6391
|
+
nodes = { ...b.nodes };
|
|
6392
|
+
this.tick(Object.keys(b.nodes).length);
|
|
6393
|
+
for (const [k, n] of this.nodes) put(nodes, k, n);
|
|
6394
|
+
this.tick(this.nodes.size);
|
|
6395
|
+
}
|
|
6396
|
+
let regions = b.regions;
|
|
6397
|
+
if (this.regions.size > 0) {
|
|
6398
|
+
regions = { ...b.regions };
|
|
6399
|
+
this.tick(Object.keys(b.regions).length);
|
|
6400
|
+
for (const [id, byPath] of this.regions) {
|
|
6401
|
+
const inner = { ...b.regions[id] };
|
|
6402
|
+
this.tick(Object.keys(inner).length);
|
|
6403
|
+
for (const [p, acc] of byPath) put(inner, p, this.materialiseRegion(acc));
|
|
6404
|
+
put(regions, id, inner);
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
return {
|
|
6408
|
+
...b,
|
|
6409
|
+
runId: this.runId,
|
|
6410
|
+
outcome: this.outcome,
|
|
6411
|
+
nodes,
|
|
6412
|
+
regions,
|
|
6413
|
+
pathEnds: this.list(b.pathEnds, this.pathEnds),
|
|
6414
|
+
haltedBy: this.haltedBy,
|
|
6415
|
+
cancelRequest: this.cancelRequest,
|
|
6416
|
+
interruption: this.interruption,
|
|
6417
|
+
paused: this.paused,
|
|
6418
|
+
resumes: this.list(b.resumes, this.resumes),
|
|
6419
|
+
gated: this.list(b.gated, this.gated)
|
|
6420
|
+
};
|
|
6421
|
+
}
|
|
6422
|
+
list(base, added) {
|
|
6423
|
+
if (added.length === 0) return base;
|
|
6424
|
+
this.tick(base.length + added.length);
|
|
6425
|
+
return [...base, ...added];
|
|
6426
|
+
}
|
|
6427
|
+
materialiseRegion(acc) {
|
|
6428
|
+
const settled = {};
|
|
6429
|
+
for (const [k, v] of acc.settled) settled[k] = v;
|
|
6430
|
+
this.tick(acc.opened.length + acc.settled.size + 1);
|
|
6431
|
+
return { ...acc.origin, opened: [...acc.opened], settled, suspended: acc.suspended.toArray(), joined: acc.joined };
|
|
6432
|
+
}
|
|
6433
|
+
};
|
|
6434
|
+
function foldWith(inputs, counter) {
|
|
6435
|
+
const acc = Accumulator.load(emptyState(), counter);
|
|
6436
|
+
for (const i of inputs) acc.apply(i);
|
|
6437
|
+
return acc.materialise();
|
|
6438
|
+
}
|
|
6439
|
+
function stepWith(prev, input, counter) {
|
|
6440
|
+
const acc = Accumulator.load(prev, counter);
|
|
6441
|
+
return acc.apply(input) ? acc.materialise() : prev;
|
|
6442
|
+
}
|
|
6443
|
+
|
|
5861
6444
|
// src/fold/run-state.ts
|
|
5862
6445
|
function emptyState() {
|
|
5863
6446
|
return {
|
|
@@ -5875,181 +6458,10 @@ function emptyState() {
|
|
|
5875
6458
|
};
|
|
5876
6459
|
}
|
|
5877
6460
|
function step(prev, input) {
|
|
5878
|
-
|
|
5879
|
-
return { ...prev, haltedBy: input.reason, outcome: "halted" };
|
|
5880
|
-
}
|
|
5881
|
-
if (input.kind === "cancelRequested") {
|
|
5882
|
-
if (prev.cancelRequest !== null || prev.outcome === "cancelled") return prev;
|
|
5883
|
-
return withOutcome({ ...prev, cancelRequest: { actor: input.actor, reason: input.reason } });
|
|
5884
|
-
}
|
|
5885
|
-
if (input.kind === "interrupted") {
|
|
5886
|
-
if (prev.interruption !== null) return prev;
|
|
5887
|
-
return withOutcome({ ...prev, interruption: { actor: input.actor, reason: input.reason } });
|
|
5888
|
-
}
|
|
5889
|
-
if (input.kind === "paused" || input.kind === "unpaused") {
|
|
5890
|
-
const paused = input.kind === "paused";
|
|
5891
|
-
if (prev.paused === paused) return prev;
|
|
5892
|
-
return { ...prev, paused };
|
|
5893
|
-
}
|
|
5894
|
-
if (input.kind === "region") {
|
|
5895
|
-
return withOutcome(applyRegion(prev, input.event));
|
|
5896
|
-
}
|
|
5897
|
-
if (input.kind === "dispatched") {
|
|
5898
|
-
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
5899
|
-
if (prev.nodes[input.taskId]) return prev;
|
|
5900
|
-
return withOutcome({
|
|
5901
|
-
...prev,
|
|
5902
|
-
runId: prev.runId ?? runId2,
|
|
5903
|
-
nodes: {
|
|
5904
|
-
...prev.nodes,
|
|
5905
|
-
[input.taskId]: { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, flowAttempt: attempt2, attemptNumber: null, state: "running", result: null, error: null }
|
|
5906
|
-
}
|
|
5907
|
-
});
|
|
5908
|
-
}
|
|
5909
|
-
if (input.kind === "asked") {
|
|
5910
|
-
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
5911
|
-
if (prev.nodes[input.taskId]) return prev;
|
|
5912
|
-
return withOutcome({
|
|
5913
|
-
...prev,
|
|
5914
|
-
runId: prev.runId ?? runId2,
|
|
5915
|
-
nodes: {
|
|
5916
|
-
...prev.nodes,
|
|
5917
|
-
[input.taskId]: {
|
|
5918
|
-
runId: runId2,
|
|
5919
|
-
nodeId: nodeId2,
|
|
5920
|
-
branchPath: branchPath2,
|
|
5921
|
-
flowAttempt: attempt2,
|
|
5922
|
-
attemptNumber: null,
|
|
5923
|
-
state: "suspended",
|
|
5924
|
-
result: null,
|
|
5925
|
-
error: null,
|
|
5926
|
-
input: structuredClone(input.input)
|
|
5927
|
-
}
|
|
5928
|
-
}
|
|
5929
|
-
});
|
|
5930
|
-
}
|
|
5931
|
-
if (input.kind === "pathEnded") {
|
|
5932
|
-
return withOutcome({ ...prev, pathEnds: [...prev.pathEnds, { reason: input.reason, at: input.at }] });
|
|
5933
|
-
}
|
|
5934
|
-
if (input.kind === "regionEntered") {
|
|
5935
|
-
if (prev.regions[input.regionId]?.[input.regionPath]) return prev;
|
|
5936
|
-
return withOutcome({
|
|
5937
|
-
...prev,
|
|
5938
|
-
regions: withRegion(prev.regions, input.regionId, input.regionPath, emptyRegion())
|
|
5939
|
-
});
|
|
5940
|
-
}
|
|
5941
|
-
if (input.kind === "resumed") {
|
|
5942
|
-
const resumes = [...prev.resumes, [...input.taskIds]];
|
|
5943
|
-
if (prev.interruption !== null) return withOutcome({ ...prev, interruption: null, resumes });
|
|
5944
|
-
return { ...prev, resumes };
|
|
5945
|
-
}
|
|
5946
|
-
if (input.kind === "gated") {
|
|
5947
|
-
const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
|
|
5948
|
-
if (input.key !== void 0) d.key = structuredClone(input.key);
|
|
5949
|
-
return { ...prev, gated: [...prev.gated, d] };
|
|
5950
|
-
}
|
|
5951
|
-
if (input.kind === "attempted") {
|
|
5952
|
-
const node = prev.nodes[input.taskId];
|
|
5953
|
-
if (node === void 0) return prev;
|
|
5954
|
-
return { ...prev, nodes: { ...prev.nodes, [input.taskId]: { ...node, attemptNumber: input.attemptNumber } } };
|
|
5955
|
-
}
|
|
5956
|
-
if (input.kind === "joinSettled") {
|
|
5957
|
-
const cur = prev.regions[input.regionId]?.[input.regionPath] ?? emptyRegion();
|
|
5958
|
-
return withOutcome({
|
|
5959
|
-
...prev,
|
|
5960
|
-
regions: withRegion(prev.regions, input.regionId, input.regionPath, { ...cur, joined: input.fired })
|
|
5961
|
-
});
|
|
5962
|
-
}
|
|
5963
|
-
const r = input.record;
|
|
5964
|
-
const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
|
|
5965
|
-
const nodes = { ...prev.nodes };
|
|
5966
|
-
const existing = nodes[r.taskId];
|
|
5967
|
-
const attemptNumber = existing?.attemptNumber ?? null;
|
|
5968
|
-
if (r.kind === "outcome") {
|
|
5969
|
-
nodes[r.taskId] = {
|
|
5970
|
-
runId,
|
|
5971
|
-
nodeId,
|
|
5972
|
-
branchPath,
|
|
5973
|
-
flowAttempt: attempt,
|
|
5974
|
-
attemptNumber,
|
|
5975
|
-
// `user_action` is `suspended` at flow level: a resolution resumes it, and
|
|
5976
|
-
// calling it terminal is what makes a design drop the fifth state.
|
|
5977
|
-
state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
|
|
5978
|
-
result: r.result,
|
|
5979
|
-
error: r.error
|
|
5980
|
-
};
|
|
5981
|
-
} else {
|
|
5982
|
-
nodes[r.taskId] = {
|
|
5983
|
-
runId: existing?.runId ?? runId,
|
|
5984
|
-
nodeId: existing?.nodeId ?? nodeId,
|
|
5985
|
-
branchPath: existing?.branchPath ?? branchPath,
|
|
5986
|
-
flowAttempt: existing?.flowAttempt ?? attempt,
|
|
5987
|
-
attemptNumber,
|
|
5988
|
-
state: r.resolvedAs,
|
|
5989
|
-
result: r.result,
|
|
5990
|
-
error: r.error,
|
|
5991
|
-
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
5992
|
-
};
|
|
5993
|
-
}
|
|
5994
|
-
return withOutcome({ ...prev, runId: prev.runId ?? runId, nodes });
|
|
5995
|
-
}
|
|
5996
|
-
function emptyRegion() {
|
|
5997
|
-
return { opened: [], settled: {}, suspended: [], joined: null };
|
|
5998
|
-
}
|
|
5999
|
-
function withRegion(regions, regionId, regionPath, run) {
|
|
6000
|
-
return { ...regions, [regionId]: { ...regions[regionId], [regionPath]: run } };
|
|
6001
|
-
}
|
|
6002
|
-
function allRegionRuns(s) {
|
|
6003
|
-
return Object.values(s.regions).flatMap((byPath) => Object.values(byPath));
|
|
6004
|
-
}
|
|
6005
|
-
function applyRegion(prev, e) {
|
|
6006
|
-
const at = prev.regions[e.regionId]?.[e.regionPath];
|
|
6007
|
-
const cur = at ? { ...at, opened: [...at.opened], settled: { ...at.settled }, suspended: [...at.suspended] } : emptyRegion();
|
|
6008
|
-
switch (e.type) {
|
|
6009
|
-
case "branch.opened":
|
|
6010
|
-
if (!cur.opened.includes(e.branchPath)) cur.opened.push(e.branchPath);
|
|
6011
|
-
break;
|
|
6012
|
-
case "branch.arrived":
|
|
6013
|
-
case "branch.exited":
|
|
6014
|
-
case "branch.lost": {
|
|
6015
|
-
const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
|
|
6016
|
-
cur.settled[e.branchPath] = how;
|
|
6017
|
-
cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
|
|
6018
|
-
break;
|
|
6019
|
-
}
|
|
6020
|
-
case "branch.suspended":
|
|
6021
|
-
if (!cur.suspended.includes(e.branchPath)) cur.suspended.push(e.branchPath);
|
|
6022
|
-
break;
|
|
6023
|
-
case "branch.resumed":
|
|
6024
|
-
cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
|
|
6025
|
-
break;
|
|
6026
|
-
}
|
|
6027
|
-
return { ...prev, regions: withRegion(prev.regions, e.regionId, e.regionPath, cur) };
|
|
6028
|
-
}
|
|
6029
|
-
function withOutcome(s) {
|
|
6030
|
-
if (s.haltedBy !== null) return { ...s, outcome: "halted" };
|
|
6031
|
-
const nodes = Object.values(s.nodes);
|
|
6032
|
-
if (s.cancelRequest === null && nodes.some((n) => n.state === "cancelled")) return { ...s, outcome: "cancelled" };
|
|
6033
|
-
if (s.cancelRequest !== null) {
|
|
6034
|
-
return { ...s, outcome: nodes.some((n) => n.state === "running") ? "cancelling" : "cancelled" };
|
|
6035
|
-
}
|
|
6036
|
-
if (s.interruption !== null) return { ...s, outcome: "interrupted" };
|
|
6037
|
-
if (nodes.some((n) => n.state === "suspended")) return { ...s, outcome: "awaiting_action" };
|
|
6038
|
-
if (nodes.some((n) => n.state === "running")) return { ...s, outcome: "running" };
|
|
6039
|
-
const openedNothing = allRegionRuns(s).some((r) => r.opened.length === 0 && r.joined !== true);
|
|
6040
|
-
if (nodes.length === 0 && s.pathEnds.length === 0 && !openedNothing) {
|
|
6041
|
-
return { ...s, outcome: "running" };
|
|
6042
|
-
}
|
|
6043
|
-
if (allRegionRuns(s).some((r) => r.joined !== true)) return { ...s, outcome: "stuck" };
|
|
6044
|
-
if (s.pathEnds.some((e) => e.reason === "no-eligible-edge")) return { ...s, outcome: "failed" };
|
|
6045
|
-
if (s.pathEnds.some((e) => e.reason === "end")) return { ...s, outcome: "completed" };
|
|
6046
|
-
if (nodes.some((n) => n.state === "failed")) return { ...s, outcome: "failed" };
|
|
6047
|
-
return { ...s, outcome: "completed" };
|
|
6461
|
+
return stepWith(prev, input);
|
|
6048
6462
|
}
|
|
6049
6463
|
function fold(inputs) {
|
|
6050
|
-
|
|
6051
|
-
for (const i of inputs) s = step(s, i);
|
|
6052
|
-
return s;
|
|
6464
|
+
return foldWith(inputs);
|
|
6053
6465
|
}
|
|
6054
6466
|
function foldLog(log) {
|
|
6055
6467
|
return fold(log.map((r) => ({ kind: r.kind, record: r })));
|
|
@@ -6264,6 +6676,7 @@ export {
|
|
|
6264
6676
|
UndispatchedTask,
|
|
6265
6677
|
MissingJoinPolicy,
|
|
6266
6678
|
Driver,
|
|
6679
|
+
Accumulator,
|
|
6267
6680
|
emptyState,
|
|
6268
6681
|
step,
|
|
6269
6682
|
fold,
|