fyflow-orchestration 0.8.0 → 0.9.1
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 +107 -7
- package/README.md +2 -1
- package/dist/browser/chunks/{chunk-EUU44PHD.js → chunk-I3KDEOLB.js} +749 -244
- package/dist/browser/index.js +3 -1
- package/dist/browser/scheduler/index.js +61 -13
- package/dist/node/chunks/{chunk-EUU44PHD.js → chunk-I3KDEOLB.js} +749 -244
- package/dist/node/index.js +3 -1
- package/dist/node/scheduler/index.js +61 -13
- package/dist/types/src/definition/regions.d.ts +12 -0
- package/dist/types/src/driver/driver.d.ts +80 -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
|
@@ -136,14 +136,14 @@ function sharedRegionDepth(regions, a, b) {
|
|
|
136
136
|
}
|
|
137
137
|
function outputIsVisible(regions, readerId, readerPath, targetId, targetPath) {
|
|
138
138
|
const depth = sharedRegionDepth(regions, readerId, targetId);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
139
|
+
const key = visibilityKey(readerPath, depth);
|
|
140
|
+
return key !== null && key === visibilityKey(targetPath, depth);
|
|
141
|
+
}
|
|
142
|
+
function visibilityKey(path, depth) {
|
|
143
|
+
if (depth === 0) return "";
|
|
144
|
+
const segs = path === "-" ? [] : path.split(".");
|
|
145
|
+
if (segs.length < depth) return null;
|
|
146
|
+
return segs.length === depth ? path : segs.slice(0, depth).join(".");
|
|
147
147
|
}
|
|
148
148
|
function branchIndexOf(regions, nodeId) {
|
|
149
149
|
const r = innermostRegionOf(regions, nodeId);
|
|
@@ -2993,6 +2993,18 @@ function describeElement(e) {
|
|
|
2993
2993
|
}
|
|
2994
2994
|
return s.length > 160 ? `${s.slice(0, 157)}...` : s;
|
|
2995
2995
|
}
|
|
2996
|
+
var NO_OUTPUTS = Object.freeze([]);
|
|
2997
|
+
function groupPosition(group, path) {
|
|
2998
|
+
let lo = 0;
|
|
2999
|
+
let hi = group.length;
|
|
3000
|
+
if (hi === 0 || compareBranchPaths(group[hi - 1].path, path) < 0) return hi;
|
|
3001
|
+
while (lo < hi) {
|
|
3002
|
+
const mid = lo + hi >>> 1;
|
|
3003
|
+
if (compareBranchPaths(group[mid].path, path) < 0) lo = mid + 1;
|
|
3004
|
+
else hi = mid;
|
|
3005
|
+
}
|
|
3006
|
+
return lo;
|
|
3007
|
+
}
|
|
2996
3008
|
var UndispatchedTask = class extends Error {
|
|
2997
3009
|
constructor(message, options) {
|
|
2998
3010
|
super(message, options);
|
|
@@ -3089,6 +3101,49 @@ var defaultCreateTask = (config) => new FyflowTask(config);
|
|
|
3089
3101
|
var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action", "task.cancelled"];
|
|
3090
3102
|
var SUBSCRIBED_EVENTS = [...SETTLEMENT_EVENTS, "task.running"];
|
|
3091
3103
|
var PASS_THROUGH = [["task.message", "onTaskMessage"], ["task.progress", "onTaskProgress"]];
|
|
3104
|
+
var NO_RECORDS = Object.freeze([]);
|
|
3105
|
+
var TaskLog = class {
|
|
3106
|
+
byTask = /* @__PURE__ */ new Map();
|
|
3107
|
+
visits = 0;
|
|
3108
|
+
/** `append`'s half: the record joins its task's sub-log, by reference. */
|
|
3109
|
+
push(record) {
|
|
3110
|
+
const sub = this.byTask.get(record.taskId);
|
|
3111
|
+
if (sub) sub.push(record);
|
|
3112
|
+
else this.byTask.set(record.taskId, [record]);
|
|
3113
|
+
}
|
|
3114
|
+
/** One task's records, in append order — what `effectiveOutcome` and `isSuspended` are handed. Read, never kept. */
|
|
3115
|
+
of(taskId) {
|
|
3116
|
+
const sub = this.byTask.get(taskId) ?? NO_RECORDS;
|
|
3117
|
+
this.visits += sub.length;
|
|
3118
|
+
return sub;
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
var Fifo = class {
|
|
3122
|
+
items = [];
|
|
3123
|
+
head = 0;
|
|
3124
|
+
get length() {
|
|
3125
|
+
return this.items.length - this.head;
|
|
3126
|
+
}
|
|
3127
|
+
push(x) {
|
|
3128
|
+
this.items.push(x);
|
|
3129
|
+
}
|
|
3130
|
+
/** The oldest element, removed — `undefined` when empty, as `Array.prototype.shift`. */
|
|
3131
|
+
shift() {
|
|
3132
|
+
if (this.head >= this.items.length) return void 0;
|
|
3133
|
+
const x = this.items[this.head];
|
|
3134
|
+
this.items[this.head] = void 0;
|
|
3135
|
+
this.head += 1;
|
|
3136
|
+
if (this.head === this.items.length) {
|
|
3137
|
+
this.items = [];
|
|
3138
|
+
this.head = 0;
|
|
3139
|
+
}
|
|
3140
|
+
return x;
|
|
3141
|
+
}
|
|
3142
|
+
/** Every element still queued, oldest first. */
|
|
3143
|
+
*[Symbol.iterator]() {
|
|
3144
|
+
for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3092
3147
|
var Driver = class {
|
|
3093
3148
|
/**
|
|
3094
3149
|
* `OR-R7` — nothing outside the inbound API writes to run state. Private, and
|
|
@@ -3106,13 +3161,21 @@ var Driver = class {
|
|
|
3106
3161
|
* channels with no shared order, so `RN-R3`'s *"read in append order"* was about
|
|
3107
3162
|
* a value no one could hold.
|
|
3108
3163
|
*
|
|
3109
|
-
* `log` above is the **record-kind subsequence of this array**,
|
|
3110
|
-
*
|
|
3111
|
-
*
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3164
|
+
* `log` above is the **record-kind subsequence of this array**, by reference — the
|
|
3165
|
+
* same record objects, never copies; `test/driver/` asserts the two agree, so the
|
|
3166
|
+
* redundancy is checked rather than trusted. *(It was kept "only because
|
|
3167
|
+
* `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
|
|
3168
|
+
* whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
|
|
3169
|
+
* It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
|
|
3170
|
+
* catch-up, and the retention walk's alias check.)*
|
|
3114
3171
|
*/
|
|
3115
3172
|
inputs = [];
|
|
3173
|
+
/**
|
|
3174
|
+
* **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
|
|
3175
|
+
* `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
|
|
3176
|
+
* `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
|
|
3177
|
+
*/
|
|
3178
|
+
taskLog = new TaskLog();
|
|
3116
3179
|
graph;
|
|
3117
3180
|
regions;
|
|
3118
3181
|
dispatched = /* @__PURE__ */ new Set();
|
|
@@ -3360,7 +3423,7 @@ var Driver = class {
|
|
|
3360
3423
|
* 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
3424
|
* appended when the step ends, in the same call order as the pauses beside it.
|
|
3362
3425
|
*/
|
|
3363
|
-
pausePending =
|
|
3426
|
+
pausePending = new Fifo();
|
|
3364
3427
|
/**
|
|
3365
3428
|
* **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
|
|
3366
3429
|
* number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
|
|
@@ -3375,7 +3438,7 @@ var Driver = class {
|
|
|
3375
3438
|
* 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
3439
|
* (`leaveStep()`): the attempt started before any of them. Empty at rest.
|
|
3377
3440
|
*/
|
|
3378
|
-
attemptsPending =
|
|
3441
|
+
attemptsPending = new Fifo();
|
|
3379
3442
|
/**
|
|
3380
3443
|
* **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
|
|
3381
3444
|
* `RN-R29`,
|
|
@@ -3874,9 +3937,7 @@ var Driver = class {
|
|
|
3874
3937
|
* handing it to a worker again would run what a person was asked to decide.
|
|
3875
3938
|
*/
|
|
3876
3939
|
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));
|
|
3940
|
+
return [...this.dispatched].filter((id) => !this.taskLog.of(id).some((r) => r.kind === "outcome"));
|
|
3880
3941
|
}
|
|
3881
3942
|
/**
|
|
3882
3943
|
* **Whether a callback fires for the element at `seq`** — `RN-R24`. Always, except during a replay,
|
|
@@ -3912,7 +3973,7 @@ var Driver = class {
|
|
|
3912
3973
|
/** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3913
3974
|
resolveStep(taskId, resolvedAs, value, midStep = false) {
|
|
3914
3975
|
if (!this.parked(taskId)) return false;
|
|
3915
|
-
|
|
3976
|
+
for (const c of this.pausePending) if (c.kind === "answer" && c.record.taskId === taskId) return false;
|
|
3916
3977
|
const { runId, nodeId, branchPath } = parseTaskId(taskId);
|
|
3917
3978
|
if (this.shut() || this.stopRequest !== null) return false;
|
|
3918
3979
|
if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
|
|
@@ -4267,7 +4328,7 @@ var Driver = class {
|
|
|
4267
4328
|
throw new RunControlRefused(
|
|
4268
4329
|
"pause",
|
|
4269
4330
|
"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.`
|
|
4331
|
+
`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
4332
|
);
|
|
4272
4333
|
}
|
|
4273
4334
|
if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
|
|
@@ -4293,7 +4354,7 @@ var Driver = class {
|
|
|
4293
4354
|
throw new RunControlRefused(
|
|
4294
4355
|
"unpause",
|
|
4295
4356
|
"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.`
|
|
4357
|
+
`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
4358
|
);
|
|
4298
4359
|
}
|
|
4299
4360
|
this.pausePending.push({ kind: "unpaused", actor: options.actor });
|
|
@@ -4301,7 +4362,7 @@ var Driver = class {
|
|
|
4301
4362
|
}
|
|
4302
4363
|
/** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
|
|
4303
4364
|
pauseWanted() {
|
|
4304
|
-
const last = this.pausePending.filter((c) => c.kind !== "answer").at(-1);
|
|
4365
|
+
const last = [...this.pausePending].filter((c) => c.kind !== "answer").at(-1);
|
|
4305
4366
|
return last !== void 0 ? last.kind === "paused" : this.paused;
|
|
4306
4367
|
}
|
|
4307
4368
|
/**
|
|
@@ -4694,7 +4755,7 @@ var Driver = class {
|
|
|
4694
4755
|
if (!node) return;
|
|
4695
4756
|
const itemIndices = this.indicesFrom(nodeId, branchPath);
|
|
4696
4757
|
const taskId = buildTaskId(runId, nodeId, branchIndices(this.pathOf(nodeId, itemIndices)));
|
|
4697
|
-
const outcome = effectiveOutcome(this.
|
|
4758
|
+
const outcome = effectiveOutcome(this.taskLog.of(taskId), taskId);
|
|
4698
4759
|
if (!outcome) return;
|
|
4699
4760
|
const result = selectEdge(
|
|
4700
4761
|
node,
|
|
@@ -5041,7 +5102,7 @@ var Driver = class {
|
|
|
5041
5102
|
* [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
5103
|
* rules 2 and 3, and `FD-C2`.
|
|
5043
5104
|
*
|
|
5044
|
-
* **It is a PROJECTION of the log and adds no retention** — measured at
|
|
5105
|
+
* **It is a PROJECTION of the log and adds no retention of a result** — measured at
|
|
5045
5106
|
* [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
|
|
5046
5107
|
* declaration and again before this was written: this driver holds
|
|
5047
5108
|
* `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
|
|
@@ -5051,14 +5112,11 @@ var Driver = class {
|
|
|
5051
5112
|
* described **today** before this method existed, and this adds **no retention**.
|
|
5052
5113
|
* *(Q19 is RESOLVED since W53 —
|
|
5053
5114
|
* [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
|
|
5115
|
+
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
|
|
5055
5116
|
* asserted by IDENTITY at ladder L2 rather than by a byte count, because a
|
|
5056
5117
|
* reachability SET cannot tell an alias from a copy. W53's readers found that
|
|
5057
5118
|
* from both ends — the rule's stated test returned a third name on a `needs`
|
|
5058
5119
|
* 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
5120
|
*
|
|
5063
5121
|
* **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
|
|
5064
5122
|
* section 3 rule 4's own reason: a resumed node has two records in the log and
|
|
@@ -5066,43 +5124,175 @@ var Driver = class {
|
|
|
5066
5124
|
* suspended has no effective outcome and therefore no entry — which is what makes
|
|
5067
5125
|
* `FD-R3` fire rather than a reference quietly resolving to `undefined`.
|
|
5068
5126
|
*
|
|
5069
|
-
* **
|
|
5070
|
-
*
|
|
5071
|
-
*
|
|
5072
|
-
*
|
|
5073
|
-
* `
|
|
5074
|
-
*
|
|
5127
|
+
* **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
|
|
5128
|
+
* [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
|
|
5129
|
+
* rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
|
|
5130
|
+
* WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
|
|
5131
|
+
* of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
|
|
5132
|
+
* construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
|
|
5133
|
+
* `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
|
|
5134
|
+
* 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
|
|
5135
|
+
* the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
|
|
5136
|
+
* (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
|
|
5137
|
+
* in its tasks' first-record order, each `result` the log record's own object.
|
|
5138
|
+
*
|
|
5139
|
+
* **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
|
|
5140
|
+
* node or an edge that declares a `needs` asks, and a run that declares none never builds it.
|
|
5075
5141
|
*/
|
|
5076
5142
|
outputs() {
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
} catch {
|
|
5092
|
-
continue;
|
|
5143
|
+
const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map(), index: /* @__PURE__ */ new Map(), depths: /* @__PURE__ */ new Map(), examined: 0 };
|
|
5144
|
+
for (; c.at < this.log.length; c.at += 1) {
|
|
5145
|
+
const r = this.log[c.at];
|
|
5146
|
+
let t = c.tasks.get(r.taskId);
|
|
5147
|
+
if (t === void 0) {
|
|
5148
|
+
let parts;
|
|
5149
|
+
try {
|
|
5150
|
+
parts = parseTaskId(r.taskId);
|
|
5151
|
+
} catch {
|
|
5152
|
+
c.tasks.set(r.taskId, null);
|
|
5153
|
+
continue;
|
|
5154
|
+
}
|
|
5155
|
+
t = { nodeId: parts.nodeId, path: parts.branchPath, order: c.tasks.size, entry: null };
|
|
5156
|
+
c.tasks.set(r.taskId, t);
|
|
5093
5157
|
}
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
else byNode.set(parts.nodeId, [{ path: parts.branchPath, result: eff.result }]);
|
|
5158
|
+
if (t === null) continue;
|
|
5159
|
+
this.place(c, t, effectiveOutcome(this.taskLog.of(r.taskId), r.taskId));
|
|
5097
5160
|
}
|
|
5098
|
-
|
|
5099
|
-
return byNode;
|
|
5161
|
+
return c.byNode;
|
|
5100
5162
|
}
|
|
5101
5163
|
/**
|
|
5102
|
-
*
|
|
5103
|
-
*
|
|
5104
|
-
*
|
|
5105
|
-
*
|
|
5164
|
+
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
5165
|
+
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
5166
|
+
* in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
|
|
5167
|
+
*
|
|
5168
|
+
* **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
|
|
5169
|
+
* RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
|
|
5170
|
+
* at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
|
|
5171
|
+
* a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
|
|
5172
|
+
* `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
|
|
5173
|
+
* fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
|
|
5174
|
+
* that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
|
|
5175
|
+
* so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
|
|
5176
|
+
* none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
|
|
5177
|
+
*/
|
|
5178
|
+
place(c, t, eff) {
|
|
5179
|
+
const list = c.byNode.get(t.nodeId);
|
|
5180
|
+
const orders = c.orders.get(t.nodeId);
|
|
5181
|
+
if (eff === null) {
|
|
5182
|
+
if (t.entry === null || list === void 0 || orders === void 0) return;
|
|
5183
|
+
const at = list.indexOf(t.entry);
|
|
5184
|
+
list.splice(at, 1);
|
|
5185
|
+
orders.splice(at, 1);
|
|
5186
|
+
this.reindex(c, t.nodeId, t.entry, null);
|
|
5187
|
+
t.entry = null;
|
|
5188
|
+
if (list.length === 0) {
|
|
5189
|
+
c.byNode.delete(t.nodeId);
|
|
5190
|
+
c.orders.delete(t.nodeId);
|
|
5191
|
+
}
|
|
5192
|
+
return;
|
|
5193
|
+
}
|
|
5194
|
+
const entry = { path: t.path, result: eff.result };
|
|
5195
|
+
if (t.entry !== null && list !== void 0) {
|
|
5196
|
+
list[list.indexOf(t.entry)] = entry;
|
|
5197
|
+
this.reindex(c, t.nodeId, t.entry, entry);
|
|
5198
|
+
t.entry = entry;
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
t.entry = entry;
|
|
5202
|
+
this.reindex(c, t.nodeId, null, entry);
|
|
5203
|
+
if (list === void 0 || orders === void 0) {
|
|
5204
|
+
c.byNode.set(t.nodeId, [entry]);
|
|
5205
|
+
c.orders.set(t.nodeId, [t.order]);
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
5208
|
+
let i = list.length;
|
|
5209
|
+
while (i > 0 && orders[i - 1] > t.order) i -= 1;
|
|
5210
|
+
list.splice(i, 0, entry);
|
|
5211
|
+
orders.splice(i, 0, t.order);
|
|
5212
|
+
}
|
|
5213
|
+
/**
|
|
5214
|
+
* **One entry's change carried into every index built for its node** — W199, ADR 0124: called by `place` at each of
|
|
5215
|
+
* its cases (a first entry, an append, an insertion at a first-record position, a replacement in place, a removal), so
|
|
5216
|
+
* the index is caught up exactly when `byNode` is and can say nothing `byNode` does not. `was` is the entry leaving
|
|
5217
|
+
* (`null` for an insertion), `now` the one arriving (`null` for a removal); a replacement keeps its path, so its
|
|
5218
|
+
* position. A group is ordered by PATH, not by first record, so where `byNode` inserted mid-list the group finds its own
|
|
5219
|
+
* place by a binary search. An entry with fewer than `depth` segments is visible to no reader at that depth and is filed
|
|
5220
|
+
* nowhere.
|
|
5221
|
+
*/
|
|
5222
|
+
reindex(c, nodeId, was, now) {
|
|
5223
|
+
const byDepth = c.index.get(nodeId);
|
|
5224
|
+
if (byDepth === void 0) return;
|
|
5225
|
+
const path = (now ?? was).path;
|
|
5226
|
+
for (const [depth, groups] of byDepth) {
|
|
5227
|
+
const key = visibilityKey(path, depth);
|
|
5228
|
+
if (key === null) continue;
|
|
5229
|
+
c.examined += 1;
|
|
5230
|
+
const group = groups.get(key);
|
|
5231
|
+
if (was !== null) {
|
|
5232
|
+
const at = groupPosition(group, path);
|
|
5233
|
+
if (now !== null) group[at] = now;
|
|
5234
|
+
else if (group.length === 1) groups.delete(key);
|
|
5235
|
+
else group.splice(at, 1);
|
|
5236
|
+
} else if (group === void 0) {
|
|
5237
|
+
groups.set(key, [now]);
|
|
5238
|
+
} else {
|
|
5239
|
+
group.splice(groupPosition(group, path), 0, now);
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
/**
|
|
5244
|
+
* **A node's outputs grouped for readers at `depth`** — built on the first read at that depth from the node's current
|
|
5245
|
+
* entries (each one examined), each group sorted ONCE, and from then on kept by `reindex`.
|
|
5246
|
+
*/
|
|
5247
|
+
groupsOf(c, nodeId, depth) {
|
|
5248
|
+
let byDepth = c.index.get(nodeId);
|
|
5249
|
+
if (byDepth === void 0) c.index.set(nodeId, byDepth = /* @__PURE__ */ new Map());
|
|
5250
|
+
let groups = byDepth.get(depth);
|
|
5251
|
+
if (groups !== void 0) return groups;
|
|
5252
|
+
groups = /* @__PURE__ */ new Map();
|
|
5253
|
+
for (const e of c.byNode.get(nodeId) ?? NO_OUTPUTS) {
|
|
5254
|
+
c.examined += 1;
|
|
5255
|
+
const key = visibilityKey(e.path, depth);
|
|
5256
|
+
if (key === null) continue;
|
|
5257
|
+
const group = groups.get(key);
|
|
5258
|
+
if (group === void 0) groups.set(key, [e]);
|
|
5259
|
+
else group.push(e);
|
|
5260
|
+
}
|
|
5261
|
+
for (const group of groups.values()) if (group.length > 1) group.sort((x, y) => compareBranchPaths(x.path, y.path));
|
|
5262
|
+
byDepth.set(depth, groups);
|
|
5263
|
+
return groups;
|
|
5264
|
+
}
|
|
5265
|
+
/** The depth `reader` and `target` share — `sharedRegionDepth`, a graph fact, asked once per pair. */
|
|
5266
|
+
depthOf(c, reader, target) {
|
|
5267
|
+
let byTarget = c.depths.get(reader);
|
|
5268
|
+
if (byTarget === void 0) c.depths.set(reader, byTarget = /* @__PURE__ */ new Map());
|
|
5269
|
+
let depth = byTarget.get(target);
|
|
5270
|
+
if (depth === void 0) byTarget.set(target, depth = sharedRegionDepth(this.regions, reader, target));
|
|
5271
|
+
return depth;
|
|
5272
|
+
}
|
|
5273
|
+
/**
|
|
5274
|
+
* **The outputs of `nodeId` visible to `atNodeId` at `here`, in `compareBranchPaths` order** — W199, ADR 0124: ADR
|
|
5275
|
+
* 0044's predicate answered by a LOOKUP. The predicate is `visibilityKey(here, d) === visibilityKey(path, d)` at the depth
|
|
5276
|
+
* `d` the two share (`outputIsVisible`), so the visible outputs are exactly the group filed under the reader's key: what
|
|
5277
|
+
* the filter over every output and the sort of its survivors returned until W199, found in the cost of its answer.
|
|
5278
|
+
* **The index's own list, never handed out** — the caller maps it to copies.
|
|
5279
|
+
*/
|
|
5280
|
+
visibleOutputs(atNodeId, here, nodeId) {
|
|
5281
|
+
this.outputs();
|
|
5282
|
+
const c = this.outputsCache;
|
|
5283
|
+
const depth = this.depthOf(c, atNodeId, nodeId);
|
|
5284
|
+
const key = visibilityKey(here, depth);
|
|
5285
|
+
if (key === null) return NO_OUTPUTS;
|
|
5286
|
+
return this.groupsOf(c, nodeId, depth).get(key) ?? NO_OUTPUTS;
|
|
5287
|
+
}
|
|
5288
|
+
/**
|
|
5289
|
+
* The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
|
|
5290
|
+
* it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
|
|
5291
|
+
* never a copy — `tasks` is one slot per task id
|
|
5292
|
+
* read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
|
|
5293
|
+
* node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
|
|
5294
|
+
* read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
|
|
5295
|
+
* *the driver computes no run state* is untouched. `null` until a `needs` first asks.
|
|
5106
5296
|
*/
|
|
5107
5297
|
outputsCache = null;
|
|
5108
5298
|
/**
|
|
@@ -5125,7 +5315,7 @@ var Driver = class {
|
|
|
5125
5315
|
compose(node, edgeValue, itemIndices, runPayload, item) {
|
|
5126
5316
|
const needs = node.needs;
|
|
5127
5317
|
if (!needs) return edgeValue;
|
|
5128
|
-
let
|
|
5318
|
+
let here = null;
|
|
5129
5319
|
const out = {};
|
|
5130
5320
|
for (const [key, ref] of Object.entries(needs)) {
|
|
5131
5321
|
const t = referenceTarget(ref);
|
|
@@ -5141,8 +5331,8 @@ var Driver = class {
|
|
|
5141
5331
|
out[key] = cloneValue(item());
|
|
5142
5332
|
continue;
|
|
5143
5333
|
}
|
|
5144
|
-
|
|
5145
|
-
const v = this.resolveReference(node.id,
|
|
5334
|
+
here ??= this.pathOf(node.id, itemIndices);
|
|
5335
|
+
const v = this.resolveReference(node.id, here, key, t, "needs");
|
|
5146
5336
|
if (v !== ABSENT) out[key] = v;
|
|
5147
5337
|
}
|
|
5148
5338
|
return out;
|
|
@@ -5163,8 +5353,8 @@ var Driver = class {
|
|
|
5163
5353
|
*
|
|
5164
5354
|
* @throws UnresolvedReference — `FD-R3`.
|
|
5165
5355
|
*/
|
|
5166
|
-
resolveReference(atNodeId, here,
|
|
5167
|
-
const visible =
|
|
5356
|
+
resolveReference(atNodeId, here, key, t, site) {
|
|
5357
|
+
const visible = this.visibleOutputs(atNodeId, here, t.node);
|
|
5168
5358
|
if (visible.length === 0 && t.conditional) {
|
|
5169
5359
|
return t.form === "all" ? [] : ABSENT;
|
|
5170
5360
|
}
|
|
@@ -5173,6 +5363,7 @@ var Driver = class {
|
|
|
5173
5363
|
`node "${atNodeId}" at branch path "${here}" declares \`${site}[${JSON.stringify(key)}]\` naming "${t.node}", and no output of that node is on this path. \`FD-V27\` checked that this node is reachable FROM it and \`FD-V29\` that it must have RUN, which is everything the graph can say; whether any of its outputs is on YOUR path is a statement about this run \u2014 a reader inside a region whose indices do not line up with the target\`s, or a join policy that fired before that node settled. FD-R3.`
|
|
5174
5364
|
);
|
|
5175
5365
|
}
|
|
5366
|
+
this.outputsCache.examined += t.form === "all" ? visible.length : 1;
|
|
5176
5367
|
return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
|
|
5177
5368
|
}
|
|
5178
5369
|
/**
|
|
@@ -5225,7 +5416,7 @@ var Driver = class {
|
|
|
5225
5416
|
const needs = edge.needs;
|
|
5226
5417
|
if (!needs) return EMPTY_NEEDS;
|
|
5227
5418
|
const here = this.pathOf(node.id, itemIndices);
|
|
5228
|
-
|
|
5419
|
+
this.outputs();
|
|
5229
5420
|
const out = {};
|
|
5230
5421
|
for (const [key, ref] of Object.entries(needs)) {
|
|
5231
5422
|
const t = referenceTarget(ref);
|
|
@@ -5240,7 +5431,7 @@ var Driver = class {
|
|
|
5240
5431
|
if (t.form === "self") {
|
|
5241
5432
|
throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
|
|
5242
5433
|
}
|
|
5243
|
-
const v = this.resolveReference(node.id, here,
|
|
5434
|
+
const v = this.resolveReference(node.id, here, key, t, "edge needs");
|
|
5244
5435
|
if (v !== ABSENT) out[key] = v;
|
|
5245
5436
|
}
|
|
5246
5437
|
return Object.freeze(out);
|
|
@@ -5332,7 +5523,7 @@ var Driver = class {
|
|
|
5332
5523
|
* predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
|
|
5333
5524
|
*/
|
|
5334
5525
|
parked(taskId) {
|
|
5335
|
-
return isSuspended(this.
|
|
5526
|
+
return isSuspended(this.taskLog.of(taskId), taskId, this.asked.has(taskId));
|
|
5336
5527
|
}
|
|
5337
5528
|
/**
|
|
5338
5529
|
* **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
|
|
@@ -5341,7 +5532,7 @@ var Driver = class {
|
|
|
5341
5532
|
* never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
|
|
5342
5533
|
*/
|
|
5343
5534
|
hasParked() {
|
|
5344
|
-
for (const id of this.dispatched) if (isSuspended(this.
|
|
5535
|
+
for (const id of this.dispatched) if (isSuspended(this.taskLog.of(id), id)) return true;
|
|
5345
5536
|
for (const id of this.asked) if (this.parked(id)) return true;
|
|
5346
5537
|
return false;
|
|
5347
5538
|
}
|
|
@@ -5619,13 +5810,18 @@ var Driver = class {
|
|
|
5619
5810
|
*
|
|
5620
5811
|
* A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
|
|
5621
5812
|
* the stored object and `OR-R8` derives rather than appends, so a second copy
|
|
5622
|
-
* here would be a second truth.
|
|
5813
|
+
* here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
|
|
5814
|
+
* by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
|
|
5815
|
+
* element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
|
|
5623
5816
|
* copy, frozen, for the same reason `snapshot()` is: a listener holding the
|
|
5624
5817
|
* driver's own array could forge a settlement and bypass `RS-R1`.
|
|
5625
5818
|
*/
|
|
5626
5819
|
append(input) {
|
|
5627
5820
|
const seq = this.inputs.push(input) - 1;
|
|
5628
|
-
if (input.kind === "outcome" || input.kind === "resolution")
|
|
5821
|
+
if (input.kind === "outcome" || input.kind === "resolution") {
|
|
5822
|
+
this.log.push(input.record);
|
|
5823
|
+
this.taskLog.push(input.record);
|
|
5824
|
+
}
|
|
5629
5825
|
const replay = this.replay;
|
|
5630
5826
|
if (replay !== null && seq < replay.stored.length) {
|
|
5631
5827
|
const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
|
|
@@ -5850,7 +6046,7 @@ var Driver = class {
|
|
|
5850
6046
|
* the way in.
|
|
5851
6047
|
*/
|
|
5852
6048
|
effectiveOutcomeOf(taskId) {
|
|
5853
|
-
const o = effectiveOutcome(this.
|
|
6049
|
+
const o = effectiveOutcome(this.taskLog.of(taskId), taskId);
|
|
5854
6050
|
return o === null ? null : cloneRecord(o);
|
|
5855
6051
|
}
|
|
5856
6052
|
terminalStateOf(taskId) {
|
|
@@ -5858,6 +6054,484 @@ var Driver = class {
|
|
|
5858
6054
|
}
|
|
5859
6055
|
};
|
|
5860
6056
|
|
|
6057
|
+
// src/fold/accumulator.ts
|
|
6058
|
+
var Suspended = class {
|
|
6059
|
+
items = [];
|
|
6060
|
+
alive = /* @__PURE__ */ new Map();
|
|
6061
|
+
gen = 0;
|
|
6062
|
+
count;
|
|
6063
|
+
constructor(from, count) {
|
|
6064
|
+
this.count = count;
|
|
6065
|
+
const g = ++this.gen;
|
|
6066
|
+
for (const v of from) {
|
|
6067
|
+
this.alive.set(v, g);
|
|
6068
|
+
this.items.push({ v, g });
|
|
6069
|
+
}
|
|
6070
|
+
count(from.length);
|
|
6071
|
+
}
|
|
6072
|
+
has(v) {
|
|
6073
|
+
return this.alive.has(v);
|
|
6074
|
+
}
|
|
6075
|
+
add(v) {
|
|
6076
|
+
if (this.alive.has(v)) return;
|
|
6077
|
+
const g = ++this.gen;
|
|
6078
|
+
this.alive.set(v, g);
|
|
6079
|
+
this.items.push({ v, g });
|
|
6080
|
+
}
|
|
6081
|
+
remove(v) {
|
|
6082
|
+
this.alive.delete(v);
|
|
6083
|
+
}
|
|
6084
|
+
toArray() {
|
|
6085
|
+
this.count(this.items.length);
|
|
6086
|
+
const out = [];
|
|
6087
|
+
for (const e of this.items) if (this.alive.get(e.v) === e.g) out.push(e.v);
|
|
6088
|
+
return out;
|
|
6089
|
+
}
|
|
6090
|
+
};
|
|
6091
|
+
function regionWeight(opened, joined) {
|
|
6092
|
+
const unjoined = joined !== true ? 1 : 0;
|
|
6093
|
+
return { unjoined, openedNothing: opened.length === 0 && joined !== true ? 1 : 0 };
|
|
6094
|
+
}
|
|
6095
|
+
var hasOwn2 = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
6096
|
+
function put(o, k, v) {
|
|
6097
|
+
if (k === "__proto__") Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
|
|
6098
|
+
else o[k] = v;
|
|
6099
|
+
}
|
|
6100
|
+
var Accumulator = class _Accumulator {
|
|
6101
|
+
runId;
|
|
6102
|
+
outcome;
|
|
6103
|
+
haltedBy;
|
|
6104
|
+
cancelRequest;
|
|
6105
|
+
interruption;
|
|
6106
|
+
paused;
|
|
6107
|
+
/** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
|
|
6108
|
+
nodes = /* @__PURE__ */ new Map();
|
|
6109
|
+
/** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
|
|
6110
|
+
regions = /* @__PURE__ */ new Map();
|
|
6111
|
+
pathEnds = [];
|
|
6112
|
+
resumes = [];
|
|
6113
|
+
gated = [];
|
|
6114
|
+
counts = null;
|
|
6115
|
+
base;
|
|
6116
|
+
counter;
|
|
6117
|
+
constructor(base, counter) {
|
|
6118
|
+
this.base = base;
|
|
6119
|
+
this.counter = counter;
|
|
6120
|
+
this.runId = base.runId;
|
|
6121
|
+
this.outcome = base.outcome;
|
|
6122
|
+
this.haltedBy = base.haltedBy;
|
|
6123
|
+
this.cancelRequest = base.cancelRequest;
|
|
6124
|
+
this.interruption = base.interruption;
|
|
6125
|
+
this.paused = base.paused;
|
|
6126
|
+
}
|
|
6127
|
+
/** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
|
|
6128
|
+
static load(state, counter) {
|
|
6129
|
+
return new _Accumulator(state, counter);
|
|
6130
|
+
}
|
|
6131
|
+
tick(n = 1) {
|
|
6132
|
+
if (this.counter !== void 0) this.counter.visits += n;
|
|
6133
|
+
}
|
|
6134
|
+
// --- the reads a runtime keeps an accumulator OPEN for — W196, ADR 0122, `RN-R44` -----------------------------
|
|
6135
|
+
/**
|
|
6136
|
+
* **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
|
|
6137
|
+
* O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
|
|
6138
|
+
*/
|
|
6139
|
+
outcomeNow() {
|
|
6140
|
+
return this.outcome;
|
|
6141
|
+
}
|
|
6142
|
+
/**
|
|
6143
|
+
* **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
|
|
6144
|
+
* An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
|
|
6145
|
+
* dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
|
|
6146
|
+
*/
|
|
6147
|
+
entry(taskId) {
|
|
6148
|
+
if (this.nodes.has(taskId)) return this.nodes.get(taskId);
|
|
6149
|
+
return hasOwn2(this.base.nodes, taskId) ? this.base.nodes[taskId] : void 0;
|
|
6150
|
+
}
|
|
6151
|
+
// --- reads, through to the base ---------------------------------------------------------------------------
|
|
6152
|
+
/** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
|
|
6153
|
+
node(taskId) {
|
|
6154
|
+
return this.nodes.has(taskId) ? this.nodes.get(taskId) : this.base.nodes[taskId];
|
|
6155
|
+
}
|
|
6156
|
+
/** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
|
|
6157
|
+
instance(regionId, regionPath) {
|
|
6158
|
+
const touched = this.regions.get(regionId)?.get(regionPath);
|
|
6159
|
+
return touched ?? this.base.regions[regionId]?.[regionPath];
|
|
6160
|
+
}
|
|
6161
|
+
/** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
|
|
6162
|
+
tally() {
|
|
6163
|
+
if (this.counts !== null) return this.counts;
|
|
6164
|
+
const c = { byState: /* @__PURE__ */ new Map(), nodes: 0, unjoined: 0, openedNothing: 0, noEligibleEdge: 0, ends: 0, pathEnds: 0 };
|
|
6165
|
+
const baseNodes = this.base.nodes;
|
|
6166
|
+
const keys = Object.keys(baseNodes);
|
|
6167
|
+
this.tick(keys.length);
|
|
6168
|
+
for (const k of keys) if (!this.nodes.has(k)) this.countNode(c, baseNodes[k], 1);
|
|
6169
|
+
for (const n of this.nodes.values()) this.countNode(c, n, 1);
|
|
6170
|
+
this.tick(this.nodes.size);
|
|
6171
|
+
const baseRegions = this.base.regions;
|
|
6172
|
+
for (const id of Object.keys(baseRegions)) {
|
|
6173
|
+
const byPath = baseRegions[id];
|
|
6174
|
+
const touched = this.regions.get(id);
|
|
6175
|
+
for (const p of Object.keys(byPath)) {
|
|
6176
|
+
this.tick();
|
|
6177
|
+
if (touched?.has(p)) continue;
|
|
6178
|
+
const r = byPath[p];
|
|
6179
|
+
const w = regionWeight(r.opened, r.joined);
|
|
6180
|
+
c.unjoined += w.unjoined;
|
|
6181
|
+
c.openedNothing += w.openedNothing;
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
for (const byPath of this.regions.values()) {
|
|
6185
|
+
for (const acc of byPath.values()) {
|
|
6186
|
+
this.tick();
|
|
6187
|
+
const w = regionWeight(acc.opened, acc.joined);
|
|
6188
|
+
c.unjoined += w.unjoined;
|
|
6189
|
+
c.openedNothing += w.openedNothing;
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
for (const list of [this.base.pathEnds, this.pathEnds]) {
|
|
6193
|
+
this.tick(list.length);
|
|
6194
|
+
for (const e of list) {
|
|
6195
|
+
c.pathEnds += 1;
|
|
6196
|
+
if (e.reason === "no-eligible-edge") c.noEligibleEdge += 1;
|
|
6197
|
+
if (e.reason === "end") c.ends += 1;
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
this.counts = c;
|
|
6201
|
+
return c;
|
|
6202
|
+
}
|
|
6203
|
+
countNode(c, n, by) {
|
|
6204
|
+
c.nodes += by;
|
|
6205
|
+
c.byState.set(n.state, (c.byState.get(n.state) ?? 0) + by);
|
|
6206
|
+
}
|
|
6207
|
+
stateCount(state) {
|
|
6208
|
+
return this.tally().byState.get(state) ?? 0;
|
|
6209
|
+
}
|
|
6210
|
+
// --- writes ------------------------------------------------------------------------------------------------
|
|
6211
|
+
/** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
|
|
6212
|
+
setNode(taskId, entry) {
|
|
6213
|
+
if (this.counts !== null) {
|
|
6214
|
+
const own3 = this.nodes.has(taskId) || hasOwn2(this.base.nodes, taskId);
|
|
6215
|
+
if (own3) this.countNode(this.counts, this.node(taskId), -1);
|
|
6216
|
+
this.countNode(this.counts, entry, 1);
|
|
6217
|
+
}
|
|
6218
|
+
this.nodes.set(taskId, entry);
|
|
6219
|
+
this.tick();
|
|
6220
|
+
}
|
|
6221
|
+
/**
|
|
6222
|
+
* The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
|
|
6223
|
+
* paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
|
|
6224
|
+
*/
|
|
6225
|
+
touchRegion(regionId, regionPath) {
|
|
6226
|
+
let byPath = this.regions.get(regionId);
|
|
6227
|
+
let acc = byPath?.get(regionPath);
|
|
6228
|
+
if (acc === void 0) {
|
|
6229
|
+
const at = this.base.regions[regionId]?.[regionPath];
|
|
6230
|
+
if (at) {
|
|
6231
|
+
this.tick(at.opened.length + Object.keys(at.settled).length);
|
|
6232
|
+
acc = {
|
|
6233
|
+
origin: at,
|
|
6234
|
+
opened: [...at.opened],
|
|
6235
|
+
openedSet: new Set(at.opened),
|
|
6236
|
+
settled: new Map(Object.entries(at.settled)),
|
|
6237
|
+
suspended: new Suspended(at.suspended, (n) => this.tick(n)),
|
|
6238
|
+
joined: at.joined
|
|
6239
|
+
};
|
|
6240
|
+
} else {
|
|
6241
|
+
acc = {
|
|
6242
|
+
origin: { opened: [], settled: {}, suspended: [], joined: null },
|
|
6243
|
+
opened: [],
|
|
6244
|
+
openedSet: /* @__PURE__ */ new Set(),
|
|
6245
|
+
settled: /* @__PURE__ */ new Map(),
|
|
6246
|
+
suspended: new Suspended([], (n) => this.tick(n)),
|
|
6247
|
+
joined: null
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
if (byPath === void 0) {
|
|
6251
|
+
byPath = /* @__PURE__ */ new Map();
|
|
6252
|
+
this.regions.set(regionId, byPath);
|
|
6253
|
+
}
|
|
6254
|
+
byPath.set(regionPath, acc);
|
|
6255
|
+
const inner = this.base.regions[regionId];
|
|
6256
|
+
if (this.counts !== null && at && inner !== void 0 && hasOwn2(this.base.regions, regionId) && hasOwn2(inner, regionPath)) {
|
|
6257
|
+
this.unweigh(regionWeight(at.opened, at.joined));
|
|
6258
|
+
}
|
|
6259
|
+
} else if (this.counts !== null) {
|
|
6260
|
+
this.unweigh(regionWeight(acc.opened, acc.joined));
|
|
6261
|
+
}
|
|
6262
|
+
this.tick();
|
|
6263
|
+
return acc;
|
|
6264
|
+
}
|
|
6265
|
+
unweigh(w) {
|
|
6266
|
+
this.counts.unjoined -= w.unjoined;
|
|
6267
|
+
this.counts.openedNothing -= w.openedNothing;
|
|
6268
|
+
}
|
|
6269
|
+
/** The instance's share of the region counts put back after a write. */
|
|
6270
|
+
settleRegion(acc) {
|
|
6271
|
+
if (this.counts === null) return;
|
|
6272
|
+
const w = regionWeight(acc.opened, acc.joined);
|
|
6273
|
+
this.counts.unjoined += w.unjoined;
|
|
6274
|
+
this.counts.openedNothing += w.openedNothing;
|
|
6275
|
+
}
|
|
6276
|
+
addPathEnd(e) {
|
|
6277
|
+
this.pathEnds.push(e);
|
|
6278
|
+
this.tick();
|
|
6279
|
+
if (this.counts !== null) {
|
|
6280
|
+
this.counts.pathEnds += 1;
|
|
6281
|
+
if (e.reason === "no-eligible-edge") this.counts.noEligibleEdge += 1;
|
|
6282
|
+
if (e.reason === "end") this.counts.ends += 1;
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
// --- the arms -------------------------------------------------------------------------------------------------
|
|
6286
|
+
/**
|
|
6287
|
+
* **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
|
|
6288
|
+
* `false` where it returned the state it was handed, so `step` can return that same object. Throws where
|
|
6289
|
+
* that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
|
|
6290
|
+
* the module header's stated limit on region paths.
|
|
6291
|
+
*/
|
|
6292
|
+
apply(input) {
|
|
6293
|
+
this.tick();
|
|
6294
|
+
if (input.kind === "halted" || input.kind === "stopped") {
|
|
6295
|
+
this.haltedBy = input.reason;
|
|
6296
|
+
this.outcome = "halted";
|
|
6297
|
+
return true;
|
|
6298
|
+
}
|
|
6299
|
+
if (input.kind === "cancelRequested") {
|
|
6300
|
+
if (this.cancelRequest !== null || this.outcome === "cancelled") return false;
|
|
6301
|
+
this.cancelRequest = { actor: input.actor, reason: input.reason };
|
|
6302
|
+
return this.withOutcome();
|
|
6303
|
+
}
|
|
6304
|
+
if (input.kind === "interrupted") {
|
|
6305
|
+
if (this.interruption !== null) return false;
|
|
6306
|
+
this.interruption = { actor: input.actor, reason: input.reason };
|
|
6307
|
+
return this.withOutcome();
|
|
6308
|
+
}
|
|
6309
|
+
if (input.kind === "paused" || input.kind === "unpaused") {
|
|
6310
|
+
const paused = input.kind === "paused";
|
|
6311
|
+
if (this.paused === paused) return false;
|
|
6312
|
+
this.paused = paused;
|
|
6313
|
+
return true;
|
|
6314
|
+
}
|
|
6315
|
+
if (input.kind === "region") {
|
|
6316
|
+
this.applyRegion(input.event);
|
|
6317
|
+
return this.withOutcome();
|
|
6318
|
+
}
|
|
6319
|
+
if (input.kind === "dispatched") {
|
|
6320
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
6321
|
+
if (this.node(input.taskId)) return false;
|
|
6322
|
+
this.runId = this.runId ?? runId2;
|
|
6323
|
+
this.setNode(input.taskId, {
|
|
6324
|
+
runId: runId2,
|
|
6325
|
+
nodeId: nodeId2,
|
|
6326
|
+
branchPath: branchPath2,
|
|
6327
|
+
flowAttempt: attempt2,
|
|
6328
|
+
attemptNumber: null,
|
|
6329
|
+
state: "running",
|
|
6330
|
+
result: null,
|
|
6331
|
+
error: null
|
|
6332
|
+
});
|
|
6333
|
+
return this.withOutcome();
|
|
6334
|
+
}
|
|
6335
|
+
if (input.kind === "asked") {
|
|
6336
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
6337
|
+
if (this.node(input.taskId)) return false;
|
|
6338
|
+
this.runId = this.runId ?? runId2;
|
|
6339
|
+
this.setNode(input.taskId, {
|
|
6340
|
+
runId: runId2,
|
|
6341
|
+
nodeId: nodeId2,
|
|
6342
|
+
branchPath: branchPath2,
|
|
6343
|
+
flowAttempt: attempt2,
|
|
6344
|
+
attemptNumber: null,
|
|
6345
|
+
state: "suspended",
|
|
6346
|
+
result: null,
|
|
6347
|
+
error: null,
|
|
6348
|
+
input: structuredClone(input.input)
|
|
6349
|
+
});
|
|
6350
|
+
return this.withOutcome();
|
|
6351
|
+
}
|
|
6352
|
+
if (input.kind === "pathEnded") {
|
|
6353
|
+
this.addPathEnd({ reason: input.reason, at: input.at });
|
|
6354
|
+
return this.withOutcome();
|
|
6355
|
+
}
|
|
6356
|
+
if (input.kind === "regionEntered") {
|
|
6357
|
+
if (this.instance(input.regionId, input.regionPath)) return false;
|
|
6358
|
+
const acc = this.touchRegion(input.regionId, input.regionPath);
|
|
6359
|
+
this.settleRegion(acc);
|
|
6360
|
+
return this.withOutcome();
|
|
6361
|
+
}
|
|
6362
|
+
if (input.kind === "resumed") {
|
|
6363
|
+
this.resumes.push([...input.taskIds]);
|
|
6364
|
+
this.tick(input.taskIds.length + 1);
|
|
6365
|
+
if (this.interruption !== null) {
|
|
6366
|
+
this.interruption = null;
|
|
6367
|
+
return this.withOutcome();
|
|
6368
|
+
}
|
|
6369
|
+
return true;
|
|
6370
|
+
}
|
|
6371
|
+
if (input.kind === "gated") {
|
|
6372
|
+
const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
|
|
6373
|
+
if (input.key !== void 0) d.key = structuredClone(input.key);
|
|
6374
|
+
this.gated.push(d);
|
|
6375
|
+
this.tick();
|
|
6376
|
+
return true;
|
|
6377
|
+
}
|
|
6378
|
+
if (input.kind === "attempted") {
|
|
6379
|
+
const node = this.node(input.taskId);
|
|
6380
|
+
if (node === void 0) return false;
|
|
6381
|
+
this.setNode(input.taskId, { ...node, attemptNumber: input.attemptNumber });
|
|
6382
|
+
return true;
|
|
6383
|
+
}
|
|
6384
|
+
if (input.kind === "joinSettled") {
|
|
6385
|
+
const acc = this.touchRegion(input.regionId, input.regionPath);
|
|
6386
|
+
acc.joined = input.fired;
|
|
6387
|
+
this.settleRegion(acc);
|
|
6388
|
+
return this.withOutcome();
|
|
6389
|
+
}
|
|
6390
|
+
const r = input.record;
|
|
6391
|
+
const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
|
|
6392
|
+
const existing = this.node(r.taskId);
|
|
6393
|
+
const attemptNumber = existing?.attemptNumber ?? null;
|
|
6394
|
+
if (r.kind === "outcome") {
|
|
6395
|
+
this.setNode(r.taskId, {
|
|
6396
|
+
runId,
|
|
6397
|
+
nodeId,
|
|
6398
|
+
branchPath,
|
|
6399
|
+
flowAttempt: attempt,
|
|
6400
|
+
attemptNumber,
|
|
6401
|
+
// `user_action` is `suspended` at flow level: a resolution resumes it.
|
|
6402
|
+
state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
|
|
6403
|
+
result: r.result,
|
|
6404
|
+
error: r.error
|
|
6405
|
+
});
|
|
6406
|
+
} else {
|
|
6407
|
+
this.setNode(r.taskId, {
|
|
6408
|
+
runId: existing?.runId ?? runId,
|
|
6409
|
+
nodeId: existing?.nodeId ?? nodeId,
|
|
6410
|
+
branchPath: existing?.branchPath ?? branchPath,
|
|
6411
|
+
flowAttempt: existing?.flowAttempt ?? attempt,
|
|
6412
|
+
attemptNumber,
|
|
6413
|
+
state: r.resolvedAs,
|
|
6414
|
+
result: r.result,
|
|
6415
|
+
error: r.error,
|
|
6416
|
+
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
6417
|
+
});
|
|
6418
|
+
}
|
|
6419
|
+
this.runId = this.runId ?? runId;
|
|
6420
|
+
return this.withOutcome();
|
|
6421
|
+
}
|
|
6422
|
+
applyRegion(e) {
|
|
6423
|
+
const acc = this.touchRegion(e.regionId, e.regionPath);
|
|
6424
|
+
switch (e.type) {
|
|
6425
|
+
case "branch.opened":
|
|
6426
|
+
if (!acc.openedSet.has(e.branchPath)) {
|
|
6427
|
+
acc.opened.push(e.branchPath);
|
|
6428
|
+
acc.openedSet.add(e.branchPath);
|
|
6429
|
+
}
|
|
6430
|
+
break;
|
|
6431
|
+
case "branch.arrived":
|
|
6432
|
+
case "branch.exited":
|
|
6433
|
+
case "branch.lost": {
|
|
6434
|
+
const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
|
|
6435
|
+
acc.settled.set(e.branchPath, how);
|
|
6436
|
+
acc.suspended.remove(e.branchPath);
|
|
6437
|
+
break;
|
|
6438
|
+
}
|
|
6439
|
+
case "branch.suspended":
|
|
6440
|
+
acc.suspended.add(e.branchPath);
|
|
6441
|
+
break;
|
|
6442
|
+
case "branch.resumed":
|
|
6443
|
+
acc.suspended.remove(e.branchPath);
|
|
6444
|
+
break;
|
|
6445
|
+
}
|
|
6446
|
+
this.settleRegion(acc);
|
|
6447
|
+
}
|
|
6448
|
+
/**
|
|
6449
|
+
* **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
|
|
6450
|
+
* one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
|
|
6451
|
+
* recomputes has moved the state.
|
|
6452
|
+
*/
|
|
6453
|
+
withOutcome() {
|
|
6454
|
+
this.outcome = this.precedence();
|
|
6455
|
+
return true;
|
|
6456
|
+
}
|
|
6457
|
+
precedence() {
|
|
6458
|
+
if (this.haltedBy !== null) return "halted";
|
|
6459
|
+
if (this.cancelRequest === null && this.stateCount("cancelled") > 0) return "cancelled";
|
|
6460
|
+
if (this.cancelRequest !== null) return this.stateCount("running") > 0 ? "cancelling" : "cancelled";
|
|
6461
|
+
if (this.interruption !== null) return "interrupted";
|
|
6462
|
+
if (this.stateCount("suspended") > 0) return "awaiting_action";
|
|
6463
|
+
if (this.stateCount("running") > 0) return "running";
|
|
6464
|
+
const c = this.tally();
|
|
6465
|
+
if (c.nodes === 0 && c.pathEnds === 0 && c.openedNothing === 0) return "running";
|
|
6466
|
+
if (c.unjoined > 0) return "stuck";
|
|
6467
|
+
if (c.noEligibleEdge > 0) return "failed";
|
|
6468
|
+
if (c.ends > 0) return "completed";
|
|
6469
|
+
if (this.stateCount("failed") > 0) return "failed";
|
|
6470
|
+
return "completed";
|
|
6471
|
+
}
|
|
6472
|
+
// --- materialisation ------------------------------------------------------------------------------------------
|
|
6473
|
+
/**
|
|
6474
|
+
* **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
|
|
6475
|
+
* 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
|
|
6476
|
+
* entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
|
|
6477
|
+
*/
|
|
6478
|
+
materialise() {
|
|
6479
|
+
const b = this.base;
|
|
6480
|
+
let nodes = b.nodes;
|
|
6481
|
+
if (this.nodes.size > 0) {
|
|
6482
|
+
nodes = { ...b.nodes };
|
|
6483
|
+
this.tick(Object.keys(b.nodes).length);
|
|
6484
|
+
for (const [k, n] of this.nodes) put(nodes, k, n);
|
|
6485
|
+
this.tick(this.nodes.size);
|
|
6486
|
+
}
|
|
6487
|
+
let regions = b.regions;
|
|
6488
|
+
if (this.regions.size > 0) {
|
|
6489
|
+
regions = { ...b.regions };
|
|
6490
|
+
this.tick(Object.keys(b.regions).length);
|
|
6491
|
+
for (const [id, byPath] of this.regions) {
|
|
6492
|
+
const inner = { ...b.regions[id] };
|
|
6493
|
+
this.tick(Object.keys(inner).length);
|
|
6494
|
+
for (const [p, acc] of byPath) put(inner, p, this.materialiseRegion(acc));
|
|
6495
|
+
put(regions, id, inner);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
return {
|
|
6499
|
+
...b,
|
|
6500
|
+
runId: this.runId,
|
|
6501
|
+
outcome: this.outcome,
|
|
6502
|
+
nodes,
|
|
6503
|
+
regions,
|
|
6504
|
+
pathEnds: this.list(b.pathEnds, this.pathEnds),
|
|
6505
|
+
haltedBy: this.haltedBy,
|
|
6506
|
+
cancelRequest: this.cancelRequest,
|
|
6507
|
+
interruption: this.interruption,
|
|
6508
|
+
paused: this.paused,
|
|
6509
|
+
resumes: this.list(b.resumes, this.resumes),
|
|
6510
|
+
gated: this.list(b.gated, this.gated)
|
|
6511
|
+
};
|
|
6512
|
+
}
|
|
6513
|
+
list(base, added) {
|
|
6514
|
+
if (added.length === 0) return base;
|
|
6515
|
+
this.tick(base.length + added.length);
|
|
6516
|
+
return [...base, ...added];
|
|
6517
|
+
}
|
|
6518
|
+
materialiseRegion(acc) {
|
|
6519
|
+
const settled = {};
|
|
6520
|
+
for (const [k, v] of acc.settled) settled[k] = v;
|
|
6521
|
+
this.tick(acc.opened.length + acc.settled.size + 1);
|
|
6522
|
+
return { ...acc.origin, opened: [...acc.opened], settled, suspended: acc.suspended.toArray(), joined: acc.joined };
|
|
6523
|
+
}
|
|
6524
|
+
};
|
|
6525
|
+
function foldWith(inputs, counter) {
|
|
6526
|
+
const acc = Accumulator.load(emptyState(), counter);
|
|
6527
|
+
for (const i of inputs) acc.apply(i);
|
|
6528
|
+
return acc.materialise();
|
|
6529
|
+
}
|
|
6530
|
+
function stepWith(prev, input, counter) {
|
|
6531
|
+
const acc = Accumulator.load(prev, counter);
|
|
6532
|
+
return acc.apply(input) ? acc.materialise() : prev;
|
|
6533
|
+
}
|
|
6534
|
+
|
|
5861
6535
|
// src/fold/run-state.ts
|
|
5862
6536
|
function emptyState() {
|
|
5863
6537
|
return {
|
|
@@ -5875,181 +6549,10 @@ function emptyState() {
|
|
|
5875
6549
|
};
|
|
5876
6550
|
}
|
|
5877
6551
|
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" };
|
|
6552
|
+
return stepWith(prev, input);
|
|
6048
6553
|
}
|
|
6049
6554
|
function fold(inputs) {
|
|
6050
|
-
|
|
6051
|
-
for (const i of inputs) s = step(s, i);
|
|
6052
|
-
return s;
|
|
6555
|
+
return foldWith(inputs);
|
|
6053
6556
|
}
|
|
6054
6557
|
function foldLog(log) {
|
|
6055
6558
|
return fold(log.map((r) => ({ kind: r.kind, record: r })));
|
|
@@ -6144,6 +6647,7 @@ export {
|
|
|
6144
6647
|
innermostMapOf,
|
|
6145
6648
|
sharedRegionDepth,
|
|
6146
6649
|
outputIsVisible,
|
|
6650
|
+
visibilityKey,
|
|
6147
6651
|
branchIndexOf,
|
|
6148
6652
|
certainlySettled,
|
|
6149
6653
|
SCHEMA_KEYWORDS,
|
|
@@ -6264,6 +6768,7 @@ export {
|
|
|
6264
6768
|
UndispatchedTask,
|
|
6265
6769
|
MissingJoinPolicy,
|
|
6266
6770
|
Driver,
|
|
6771
|
+
Accumulator,
|
|
6267
6772
|
emptyState,
|
|
6268
6773
|
step,
|
|
6269
6774
|
fold,
|