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.
@@ -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**, kept as its own
3110
- * array only because `effectiveOutcome` and `isSuspended` scan it on every
3111
- * settlement and rebuilding it by filter each time is quadratic. It holds the
3112
- * same record objects, never copies. `test/driver/` asserts the two agree, so
3113
- * the redundancy is checked rather than trusted.
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
- const settled = /* @__PURE__ */ new Set();
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
- if (this.pausePending.some((c) => c.kind === "answer" && c.record.taskId === taskId)) return false;
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.log, taskId);
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 this memo by id: it holds results **by reference only**,
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
- * **Computed on demand rather than maintained**, and the cost is why: it is
5070
- * `O(log)` per call, so maintaining it would be a second source that could
5071
- * disagree with the log — the defect [Q43](../../doc/questions/q43.md) and
5072
- * `arrival-count`'s re-sourcing are each an instance of. **A node that declares no
5073
- * `needs` never reaches this method**, which is what keeps ADR 0040 rule 1's *the
5074
- * default is unchanged* true of the cost as well as of the value.
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
- if (this.outputsCache && this.outputsCache.at === this.log.length) return this.outputsCache.byNode;
5078
- const byNode = /* @__PURE__ */ new Map();
5079
- const groups = /* @__PURE__ */ new Map();
5080
- for (const r of this.log) {
5081
- const g = groups.get(r.taskId);
5082
- if (g) g.push(r);
5083
- else groups.set(r.taskId, [r]);
5084
- }
5085
- for (const [taskId, group] of groups) {
5086
- const eff = effectiveOutcome(group, taskId);
5087
- if (!eff) continue;
5088
- let parts;
5089
- try {
5090
- parts = parseTaskId(taskId);
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
- const list = byNode.get(parts.nodeId);
5095
- if (list) list.push({ path: parts.branchPath, result: eff.result });
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
- this.outputsCache = { at: this.log.length, byNode };
5099
- return byNode;
5149
+ return c.byNode;
5100
5150
  }
5101
5151
  /**
5102
- * The memo above. **Not run state and not a second copy of it** — it is discarded
5103
- * and rebuilt the moment the log grows, and nothing ever reads it without checking
5104
- * the length first, so there is no state here that could be stale in a way anything
5105
- * could observe. `ADR 0014`'s *the driver computes no run state* is untouched.
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.log, taskId, this.asked.has(taskId));
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.log, id)) return true;
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. What leaves through `onFoldInput` **is** a
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") this.log.push(input.record);
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.log, taskId);
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
- if (input.kind === "halted" || input.kind === "stopped") {
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
- let s = emptyState();
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,