fyflow-orchestration 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +160 -24
- package/README.md +1 -1
- package/dist/browser/chunks/{chunk-3U4UZC34.js → chunk-NA6QQAZ2.js} +469 -113
- package/dist/browser/index.js +5 -1
- package/dist/browser/scheduler/index.js +13 -2
- package/dist/node/chunks/{chunk-3U4UZC34.js → chunk-NA6QQAZ2.js} +469 -113
- package/dist/node/index.js +5 -1
- package/dist/node/scheduler/index.js +13 -2
- package/dist/types/src/definition/regions.d.ts +12 -0
- package/dist/types/src/driver/driver.d.ts +185 -10
- package/dist/types/src/scheduler/runtime.d.ts +17 -1
- 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);
|
|
@@ -2764,6 +2764,74 @@ function isTerminalEvent(e) {
|
|
|
2764
2764
|
return TERMINAL_EVENTS.includes(e.type);
|
|
2765
2765
|
}
|
|
2766
2766
|
|
|
2767
|
+
// src/fold/policies.ts
|
|
2768
|
+
var arrivalCount = {
|
|
2769
|
+
id: "arrival-count",
|
|
2770
|
+
// **`region` is deliberately unread.** It was the source of `need` and is now
|
|
2771
|
+
// the source of nothing: `branches` cannot answer *how many branches did this
|
|
2772
|
+
// region open* for a map, and a policy that read it for a fanout and counted for
|
|
2773
|
+
// a map would be branching on the node kind — which it cannot see
|
|
2774
|
+
// ([Q159](../../doc/questions/q159.md)) and, per that row, should not.
|
|
2775
|
+
open: () => ({ need: 0, arrived: [] }),
|
|
2776
|
+
step: (s, e) => {
|
|
2777
|
+
if (e.type === "branch.opened") return { ...s, need: s.need + 1 };
|
|
2778
|
+
if (e.type !== "branch.arrived" || s.arrived.includes(e.branchPath)) return s;
|
|
2779
|
+
return { ...s, arrived: [...s.arrived, e.branchPath] };
|
|
2780
|
+
},
|
|
2781
|
+
// **`s.need > 0` is a PIN, not a refinement**, and it is exactly `JP-R8`'s own
|
|
2782
|
+
// *"having been positive"* argument in the policy that argument was denied to.
|
|
2783
|
+
// A bare `arrived.length >= need` is true at `0 >= 0`, so without it `open`
|
|
2784
|
+
// itself reports `satisfied` — and at a map of zero items this policy would fire
|
|
2785
|
+
// a join **no branch ever reached**, which is character for character what
|
|
2786
|
+
// [ADR 0025](../../doc/decisions/0025-the-zero-arrival-join-is-reportable-and-arrival-counting-is-not-repaired.md)
|
|
2787
|
+
// REFUSED `JP-R7`'s variant for. It is vacuous on every `fanout` region, because
|
|
2788
|
+
// `FD-V9`'s floor puts `need` at 2 or more.
|
|
2789
|
+
status: (s) => s.need > 0 && s.arrived.length >= s.need ? "satisfied" : "waiting",
|
|
2790
|
+
// Never `unsatisfiable`: this policy has no way to know a join can never fire,
|
|
2791
|
+
// which is precisely the argument Q4 states as a testable claim. **At a map of
|
|
2792
|
+
// zero items it says `waiting`, exactly as the other two do** — the region is
|
|
2793
|
+
// reported dead by the FOLD (`RN-R10`, via the `regionEntered` input) rather
|
|
2794
|
+
// than by any policy, which is `spec/join-policy.md` section 5's own case.
|
|
2795
|
+
describe: (s) => ({ need: s.need, arrived: [...s.arrived] })
|
|
2796
|
+
};
|
|
2797
|
+
var liveToken = {
|
|
2798
|
+
id: "live-token",
|
|
2799
|
+
open: () => ({ live: 0, positive: false }),
|
|
2800
|
+
step: (s, e) => {
|
|
2801
|
+
if (e.type === "branch.opened") return { live: s.live + 1, positive: true };
|
|
2802
|
+
if (e.type === "branch.arrived" || e.type === "branch.exited" || e.type === "branch.lost") {
|
|
2803
|
+
return { ...s, live: s.live - 1 };
|
|
2804
|
+
}
|
|
2805
|
+
return s;
|
|
2806
|
+
},
|
|
2807
|
+
status: (s) => s.positive && s.live === 0 ? "satisfied" : "waiting",
|
|
2808
|
+
// NOT requiring an arrival before reporting `satisfied`, and NOT returning
|
|
2809
|
+
// `unsatisfiable` on zero-with-no-arrivals. That is JP-R8's third policy, and
|
|
2810
|
+
// it is the one that would hand live-token a graceful answer arrival-count is
|
|
2811
|
+
// structurally denied — which is the asymmetry JP-R8 exists to forbid.
|
|
2812
|
+
describe: (s) => ({ live: s.live, positive: s.positive })
|
|
2813
|
+
};
|
|
2814
|
+
var liveTokenRequiringArrival = {
|
|
2815
|
+
id: "live-token-requiring-arrival",
|
|
2816
|
+
open: () => ({ live: 0, positive: false, arrived: 0 }),
|
|
2817
|
+
step: (s, e) => {
|
|
2818
|
+
if (e.type === "branch.opened") return { ...s, live: s.live + 1, positive: true };
|
|
2819
|
+
if (e.type === "branch.arrived") return { ...s, live: s.live - 1, arrived: s.arrived + 1 };
|
|
2820
|
+
if (e.type === "branch.exited" || e.type === "branch.lost") return { ...s, live: s.live - 1 };
|
|
2821
|
+
return s;
|
|
2822
|
+
},
|
|
2823
|
+
status: (s) => {
|
|
2824
|
+
if (!s.positive || s.live !== 0) return "waiting";
|
|
2825
|
+
return s.arrived > 0 ? "satisfied" : "unsatisfiable";
|
|
2826
|
+
},
|
|
2827
|
+
describe: (s) => ({ live: s.live, positive: s.positive, arrived: s.arrived })
|
|
2828
|
+
};
|
|
2829
|
+
var POLICIES = {
|
|
2830
|
+
"arrival-count": arrivalCount,
|
|
2831
|
+
"live-token": liveToken,
|
|
2832
|
+
"live-token-requiring-arrival": liveTokenRequiringArrival
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2767
2835
|
// src/driver/driver.ts
|
|
2768
2836
|
import { FyflowTask } from "fyflow-scheduler";
|
|
2769
2837
|
var InvalidDefinition = class _InvalidDefinition extends Error {
|
|
@@ -2879,6 +2947,12 @@ var ResumeRefused = class extends Error {
|
|
|
2879
2947
|
}
|
|
2880
2948
|
};
|
|
2881
2949
|
function resumeRefusal(definition, checkpoint, runId) {
|
|
2950
|
+
const header = headerRefusal(definition, checkpoint, runId);
|
|
2951
|
+
if (header !== null) return header;
|
|
2952
|
+
const cp = checkpoint;
|
|
2953
|
+
return haltRefusal(cp.events);
|
|
2954
|
+
}
|
|
2955
|
+
function headerRefusal(definition, checkpoint, runId) {
|
|
2882
2956
|
const cp = checkpoint;
|
|
2883
2957
|
const h = typeof cp === "object" && cp !== null ? cp.header : void 0;
|
|
2884
2958
|
if (typeof h !== "object" || h === null || !Array.isArray(cp.events)) {
|
|
@@ -2916,24 +2990,27 @@ function resumeRefusal(definition, checkpoint, runId) {
|
|
|
2916
2990
|
`ResumeRefused (flow): the checkpoint is of flow ${JSON.stringify(h.flowId)} version ${JSON.stringify(h.version)}, and this driver runs "${definition.flowId}" version ${definition.version}. There is no migration (P23 decision 7): a checkpoint resumes against the definition that produced it, and a changed flow is a new version.`
|
|
2917
2991
|
);
|
|
2918
2992
|
}
|
|
2919
|
-
|
|
2993
|
+
return null;
|
|
2994
|
+
}
|
|
2995
|
+
function haltRefusal(events) {
|
|
2996
|
+
const at = events.findIndex((e) => {
|
|
2920
2997
|
const k = e?.kind;
|
|
2921
2998
|
return k === "halted" || k === "stopped";
|
|
2922
2999
|
});
|
|
2923
3000
|
if (at >= 0) {
|
|
2924
|
-
const e =
|
|
3001
|
+
const e = events[at];
|
|
2925
3002
|
return new ResumeRefused(
|
|
2926
3003
|
"halted",
|
|
2927
3004
|
at,
|
|
2928
3005
|
`ResumeRefused (halted): the stored run was HALTED by a refusal \u2014 its element at position ${at} is \`${String(e.kind)}\`${e.kind === "stopped" ? " (the name 0.7.0 wrote for `halted`, ADR 0102)" : ""} (${JSON.stringify(e.reason)}). A halted run is not resumable (P23 decision 10): the refusal that halted it would halt it again, and the one class whose cause never reached the log \u2014 a result OR-R6 refused \u2014 cannot be regenerated at all.`
|
|
2929
3006
|
);
|
|
2930
3007
|
}
|
|
2931
|
-
const c =
|
|
3008
|
+
const c = events.findIndex((e) => {
|
|
2932
3009
|
const x = e;
|
|
2933
3010
|
return x?.kind === "cancelRequested" || x?.kind === "outcome" && x.record?.terminalState === "cancelled";
|
|
2934
3011
|
});
|
|
2935
3012
|
if (c >= 0) {
|
|
2936
|
-
const e =
|
|
3013
|
+
const e = events[c];
|
|
2937
3014
|
const what = e?.kind === "cancelRequested" ? `the request of ${JSON.stringify(e.actor)} to cancel it (\`cancelRequested\`)` : `the settlement of ${JSON.stringify(e?.record?.taskId)} as \`cancelled\``;
|
|
2938
3015
|
return new ResumeRefused(
|
|
2939
3016
|
"cancelled",
|
|
@@ -2993,6 +3070,46 @@ function describeElement(e) {
|
|
|
2993
3070
|
}
|
|
2994
3071
|
return s.length > 160 ? `${s.slice(0, 157)}...` : s;
|
|
2995
3072
|
}
|
|
3073
|
+
function deepFreeze(root) {
|
|
3074
|
+
const stack = [root];
|
|
3075
|
+
while (stack.length > 0) {
|
|
3076
|
+
const v = stack.pop();
|
|
3077
|
+
if (v === null || typeof v !== "object" || ArrayBuffer.isView(v) || v instanceof RegExp || Object.isFrozen(v)) continue;
|
|
3078
|
+
Object.freeze(v);
|
|
3079
|
+
const proto = Object.getPrototypeOf(v);
|
|
3080
|
+
if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) continue;
|
|
3081
|
+
for (const k of Object.keys(v)) {
|
|
3082
|
+
const x = v[k];
|
|
3083
|
+
if (x !== null && typeof x === "object") stack.push(x);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
function sharedCopy(v) {
|
|
3088
|
+
const copy = cloneValue(v);
|
|
3089
|
+
deepFreeze(copy);
|
|
3090
|
+
return copy;
|
|
3091
|
+
}
|
|
3092
|
+
var NO_OUTPUTS = Object.freeze([]);
|
|
3093
|
+
function groupPosition(group, path) {
|
|
3094
|
+
let lo = 0;
|
|
3095
|
+
let hi = group.length;
|
|
3096
|
+
if (hi === 0 || compareBranchPaths(group[hi - 1].path, path) < 0) return hi;
|
|
3097
|
+
while (lo < hi) {
|
|
3098
|
+
const mid = lo + hi >>> 1;
|
|
3099
|
+
if (compareBranchPaths(group[mid].path, path) < 0) lo = mid + 1;
|
|
3100
|
+
else hi = mid;
|
|
3101
|
+
}
|
|
3102
|
+
return lo;
|
|
3103
|
+
}
|
|
3104
|
+
var InputCut = class extends Error {
|
|
3105
|
+
};
|
|
3106
|
+
var DEAF_SCHEDULER = {
|
|
3107
|
+
addTask() {
|
|
3108
|
+
throw new Error("a reading hands nothing to a scheduler (RN-R45, ADR 0128) \u2014 this is a defect in the driver");
|
|
3109
|
+
},
|
|
3110
|
+
addEventListener() {
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
2996
3113
|
var UndispatchedTask = class extends Error {
|
|
2997
3114
|
constructor(message, options) {
|
|
2998
3115
|
super(message, options);
|
|
@@ -3132,7 +3249,7 @@ var Fifo = class {
|
|
|
3132
3249
|
for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
|
|
3133
3250
|
}
|
|
3134
3251
|
};
|
|
3135
|
-
var Driver = class {
|
|
3252
|
+
var Driver = class _Driver {
|
|
3136
3253
|
/**
|
|
3137
3254
|
* `OR-R7` — nothing outside the inbound API writes to run state. Private, and
|
|
3138
3255
|
* `snapshot()` is the only way out: a public `readonly` array still accepts
|
|
@@ -3264,7 +3381,8 @@ var Driver = class {
|
|
|
3264
3381
|
* - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
|
|
3265
3382
|
* by a run and by a resume's replay alike, so a replay REBUILDS it (`RN-R19`; P38's brief: *taken at rest*, never by
|
|
3266
3383
|
* a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
|
|
3267
|
-
* `needs` composition's
|
|
3384
|
+
* `needs` composition's value — since W202 an element of a reference's SHARED copy, which this keeps alive while held),
|
|
3385
|
+
* and as its OWN copy only where `kept` says that element is the caller's
|
|
3268
3386
|
* (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
|
|
3269
3387
|
* **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
|
|
3270
3388
|
* `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
|
|
@@ -3686,7 +3804,7 @@ var Driver = class {
|
|
|
3686
3804
|
const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
|
|
3687
3805
|
const schema = worker?.inputSchema;
|
|
3688
3806
|
if (schema === void 0) continue;
|
|
3689
|
-
const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
|
|
3807
|
+
const received = this.compose(n, value, EMPTY_INDICES, () => cloneValue(payload), () => value);
|
|
3690
3808
|
const fs = findings(schema, received);
|
|
3691
3809
|
if (fs.length) {
|
|
3692
3810
|
throw new BoundaryRefused(
|
|
@@ -3783,7 +3901,7 @@ var Driver = class {
|
|
|
3783
3901
|
const entryPayload = structuredClone(checkpoint.header.entryPayload);
|
|
3784
3902
|
this.header = { format: CHECKPOINT_FORMAT, runId: this.runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
|
|
3785
3903
|
this.started = true;
|
|
3786
|
-
const replay = { stored, feeding: -1, held: /* @__PURE__ */ new Map() };
|
|
3904
|
+
const replay = { stored, feeding: -1, held: /* @__PURE__ */ new Map(), reading: null };
|
|
3787
3905
|
this.replay = replay;
|
|
3788
3906
|
try {
|
|
3789
3907
|
this.replayStored(stored);
|
|
@@ -3848,6 +3966,7 @@ var Driver = class {
|
|
|
3848
3966
|
if (!isFed(e)) continue;
|
|
3849
3967
|
if (this.inputs.length !== i) throw this.diverged(this.inputs.length, "the replay produced nothing there");
|
|
3850
3968
|
replay.feeding = i;
|
|
3969
|
+
if (replay.reading !== null) replay.reading.fed += 1;
|
|
3851
3970
|
this.feed(() => this.feedOne(e, i));
|
|
3852
3971
|
}
|
|
3853
3972
|
if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
|
|
@@ -3901,12 +4020,15 @@ var Driver = class {
|
|
|
3901
4020
|
this.appendResolution(cloneRecord(record), nodeId, branchPath);
|
|
3902
4021
|
this.advance(runId, nodeId, branchPath);
|
|
3903
4022
|
}
|
|
3904
|
-
/**
|
|
4023
|
+
/**
|
|
4024
|
+
* The listener's boundary, for the replay: a refusal routing raises stops the run (`OR-R10`); a refused resume passes
|
|
4025
|
+
* through — and so does a reading's cut (`InputCut`, W205), which is no refusal and halts nothing.
|
|
4026
|
+
*/
|
|
3905
4027
|
feed(step2) {
|
|
3906
4028
|
try {
|
|
3907
4029
|
step2();
|
|
3908
4030
|
} catch (e) {
|
|
3909
|
-
if (e instanceof ResumeRefused) throw e;
|
|
4031
|
+
if (e instanceof ResumeRefused || e instanceof InputCut) throw e;
|
|
3910
4032
|
this.failRun(e);
|
|
3911
4033
|
}
|
|
3912
4034
|
}
|
|
@@ -3919,6 +4041,119 @@ var Driver = class {
|
|
|
3919
4041
|
`ResumeRefused (diverged): replaying run "${this.runId}" does not reproduce the stored element at position ${at} \u2014 stored ${describeElement(stored[at])}, replayed ${describeElement(this.inputs[at])} (${why}). P23 decision 9: a stored run the replay cannot regenerate \u2014 a predicate, a definition or an entry payload that is not what produced it \u2014 is refused rather than resumed on a different road. Nothing was handed to the scheduler.`
|
|
3920
4042
|
);
|
|
3921
4043
|
}
|
|
4044
|
+
/**
|
|
4045
|
+
* **A cut site offers what a node was handed** (`RN-R45`, W205). An offer for another path is dropped. `own` is the
|
|
4046
|
+
* position of the node's OWN element where it is already appended — a task's `dispatched`, a join's firing — and `null`
|
|
4047
|
+
* where it is the NEXT append, which `append` confirms (`Reading.pending`). Either way it is TAKEN only at a STORED
|
|
4048
|
+
* position, so a composition whose element lies past the prefix the reading verifies is never answered.
|
|
4049
|
+
*/
|
|
4050
|
+
offer(r, input, own3) {
|
|
4051
|
+
if (r.branchPath !== null && input.branchPath !== r.branchPath) return;
|
|
4052
|
+
const stored = this.replay.stored.length;
|
|
4053
|
+
if (own3 === null) {
|
|
4054
|
+
if (this.inputs.length < stored) r.pending = { at: this.inputs.length, input };
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
if (own3 < stored) this.take(r, input);
|
|
4058
|
+
}
|
|
4059
|
+
/** **An offer taken** — kept, and for a target with a path the replay ends HERE: nothing after it is replayed (`InputCut`). */
|
|
4060
|
+
take(r, input) {
|
|
4061
|
+
r.found.push(input);
|
|
4062
|
+
if (r.branchPath !== null) throw new InputCut();
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* **A READING — what a node was handed, by replaying the run to that node's composition** — `RN-R45`, W205,
|
|
4066
|
+
* [ADR 0128](../../doc/decisions/0128-what-a-node-was-handed-is-read-by-replaying-the-run-to-its-composition.md), P42
|
|
4067
|
+
* decisions 1-7. The export {@link inputOf} and `RunHandle.inputOf` are this and nothing else. In order:
|
|
4068
|
+
*
|
|
4069
|
+
* 1. **The header**, refused by `resumeRefusal`'s own header half (`format`, `flow`) — never its halt half.
|
|
4070
|
+
* 2. **The target**, refused `unknown` where it cannot name anything in this run.
|
|
4071
|
+
* 3. **A driver built exactly as `runtime.resume` builds one** (`RN-R25`) — the definition, the registry's members, a
|
|
4072
|
+
* `joinPolicy` defaulting to `liveToken` — with NO callback and a scheduler that is handed nothing (`DEAF_SCHEDULER`).
|
|
4073
|
+
* 4. **The replay** (`readStep`) over the stored PREFIX it can verify, the cut armed: it stops at the target's
|
|
4074
|
+
* composition and returns what it found.
|
|
4075
|
+
*
|
|
4076
|
+
* Private and STATIC, so the export and the handle reach one road, and a test reaches the reading's own driver — its
|
|
4077
|
+
* count — by a cast (ADR 0121 rule 7): `driver` is `null` where a refusal came before one was built.
|
|
4078
|
+
*/
|
|
4079
|
+
static read(flow, options, run, target) {
|
|
4080
|
+
const refused = headerRefusal(flow, run);
|
|
4081
|
+
if (refused !== null) {
|
|
4082
|
+
return { answer: { ok: false, reason: refused.reason, index: null, message: `a reading refuses this header as a resume does \u2014 ${refused.message}` }, driver: null };
|
|
4083
|
+
}
|
|
4084
|
+
const header = run.header;
|
|
4085
|
+
try {
|
|
4086
|
+
assertRunId(header.runId);
|
|
4087
|
+
} catch (e) {
|
|
4088
|
+
return { answer: { ok: false, reason: "format", index: null, message: `the stored header's runId is not a run id (ID-R2): ${e instanceof Error ? e.message : String(e)}` }, driver: null };
|
|
4089
|
+
}
|
|
4090
|
+
const t = readTarget(flow, header.runId, target);
|
|
4091
|
+
if ("ok" in t) return { answer: t, driver: null };
|
|
4092
|
+
const { predicates, workers, nodeTypes, pools, groups, languages, gateTypes, joinPolicy, defaultRetryPolicy, validateBoundaries } = options;
|
|
4093
|
+
const driver = new _Driver(flow, {
|
|
4094
|
+
runId: header.runId,
|
|
4095
|
+
scheduler: DEAF_SCHEDULER,
|
|
4096
|
+
predicates,
|
|
4097
|
+
...workers !== void 0 ? { workers } : {},
|
|
4098
|
+
...nodeTypes !== void 0 ? { nodeTypes } : {},
|
|
4099
|
+
...pools !== void 0 ? { pools } : {},
|
|
4100
|
+
...groups !== void 0 ? { groups } : {},
|
|
4101
|
+
...languages !== void 0 ? { languages } : {},
|
|
4102
|
+
...gateTypes !== void 0 ? { gateTypes } : {},
|
|
4103
|
+
joinPolicy: joinPolicy ?? liveToken,
|
|
4104
|
+
...defaultRetryPolicy !== void 0 ? { defaultRetryPolicy } : {},
|
|
4105
|
+
...validateBoundaries !== void 0 ? { validateBoundaries } : {}
|
|
4106
|
+
});
|
|
4107
|
+
return { answer: driver.readStep(run, t), driver };
|
|
4108
|
+
}
|
|
4109
|
+
/**
|
|
4110
|
+
* **The reading's replay** — `resume()`'s first half (`resumeStep` to `replayStored`) with the cut armed, and nothing of
|
|
4111
|
+
* its second: no in-flight set, no `resumed`, nothing handed on. **The prefix it verifies ENDS at the first `halted`,
|
|
4112
|
+
* `stopped` or `cancelRequested` element** (P42 decision 5): after one a halted driver composes nothing more, whether the
|
|
4113
|
+
* replay regenerates it (a predicate's throw) or cannot (a consumer's callback, a refused result, an owner's call) —
|
|
4114
|
+
* what follows is `not-reached`, never `diverged` (P42 retro (a1) corrected the reason). Everything
|
|
4115
|
+
* before is compared as `RN-R20` compares it.
|
|
4116
|
+
*/
|
|
4117
|
+
readStep(run, t) {
|
|
4118
|
+
const events = run.events;
|
|
4119
|
+
let end = events.length;
|
|
4120
|
+
for (let i = 0; i < events.length; i += 1) {
|
|
4121
|
+
const e = events[i];
|
|
4122
|
+
if (typeof e !== "object" || e === null) {
|
|
4123
|
+
return { ok: false, reason: "format", index: null, message: `the stored element at position ${i} is ${JSON.stringify(e) ?? String(e)}, which is not a fold input (RN-R14, RN-R17)` };
|
|
4124
|
+
}
|
|
4125
|
+
const k = e.kind;
|
|
4126
|
+
if (k === "halted" || k === "stopped" || k === "cancelRequested") {
|
|
4127
|
+
end = i;
|
|
4128
|
+
break;
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
let entryPayload;
|
|
4132
|
+
try {
|
|
4133
|
+
entryPayload = structuredClone(run.header.entryPayload);
|
|
4134
|
+
} catch (e) {
|
|
4135
|
+
return { ok: false, reason: "format", index: null, message: `the stored header's entry payload does not clone (RN-R15, RN-R17): ${e instanceof Error ? e.message : String(e)}` };
|
|
4136
|
+
}
|
|
4137
|
+
this.header = { format: CHECKPOINT_FORMAT, runId: this.runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
|
|
4138
|
+
this.started = true;
|
|
4139
|
+
const reading = { nodeId: t.nodeId, branchPath: t.branchPath, pending: null, found: [], fed: 0, elements: 0 };
|
|
4140
|
+
this.replay = { stored: end === events.length ? events : events.slice(0, end), feeding: -1, held: /* @__PURE__ */ new Map(), reading };
|
|
4141
|
+
try {
|
|
4142
|
+
this.replayStored(this.replay.stored);
|
|
4143
|
+
} catch (e) {
|
|
4144
|
+
if (e instanceof InputCut) return { ok: true, inputs: [...reading.found] };
|
|
4145
|
+
if (e instanceof ResumeRefused) return { ok: false, reason: "diverged", index: e.index, message: e.message };
|
|
4146
|
+
throw e;
|
|
4147
|
+
}
|
|
4148
|
+
if (reading.found.length > 0) return { ok: true, inputs: [...reading.found] };
|
|
4149
|
+
const where = t.branchPath === null ? `node "${t.nodeId}" on any branch path` : `node "${t.nodeId}" at branch path "${t.branchPath}"`;
|
|
4150
|
+
return {
|
|
4151
|
+
ok: false,
|
|
4152
|
+
reason: "not-reached",
|
|
4153
|
+
index: end,
|
|
4154
|
+
message: `the replay handed ${where} nothing within the ${end} stored element(s) a reading verifies` + (end < events.length ? ` \u2014 the prefix ends at position ${end}, \`${String(events[end].kind)}\`, after which a halted driver composes nothing more (RN-R45)` : "") + "."
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
3922
4157
|
/**
|
|
3923
4158
|
* **What is IN FLIGHT: dispatched, and no settlement in the log** — `RN-R21`, in dispatch order. A
|
|
3924
4159
|
* `user_action` settlement IS a settlement: a parked task is waiting on a person, not on a worker, and
|
|
@@ -4792,7 +5027,12 @@ var Driver = class {
|
|
|
4792
5027
|
if (this.shut()) return;
|
|
4793
5028
|
const node = this.graph.byId.get(nodeId);
|
|
4794
5029
|
if (!node) return;
|
|
4795
|
-
const received = this.compose(node, payload, itemIndices, this.
|
|
5030
|
+
const received = this.compose(node, payload, itemIndices, () => this.sharedPayload(), () => this.itemOf(node.id, itemIndices));
|
|
5031
|
+
const reading = this.replay?.reading ?? null;
|
|
5032
|
+
if (reading !== null && node.kind !== "task" && node.id === reading.nodeId) {
|
|
5033
|
+
const path = node.kind === "join" || node.kind === "end" ? fromBranch : this.pathOf(node.id, itemIndices);
|
|
5034
|
+
this.offer(reading, { nodeId: node.id, kind: node.kind, branchPath: path, handed: node.kind === "join" ? "arrival" : "received", value: received }, null);
|
|
5035
|
+
}
|
|
4796
5036
|
switch (node.kind) {
|
|
4797
5037
|
case "task":
|
|
4798
5038
|
return this.dispatch(runId, node, received, itemIndices);
|
|
@@ -5100,7 +5340,8 @@ var Driver = class {
|
|
|
5100
5340
|
* described **today** before this method existed, and this adds **no retention**.
|
|
5101
5341
|
* *(Q19 is RESOLVED since W53 —
|
|
5102
5342
|
* [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
|
|
5103
|
-
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference
|
|
5343
|
+
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference** — and since W202 (ADR 0126) one shared
|
|
5344
|
+
* COPY per output a reference has read, the one thing in it that is not the log's object,
|
|
5104
5345
|
* asserted by IDENTITY at ladder L2 rather than by a byte count, because a
|
|
5105
5346
|
* reachability SET cannot tell an alias from a copy. W53's readers found that
|
|
5106
5347
|
* from both ends — the rule's stated test returned a third name on a `needs`
|
|
@@ -5128,7 +5369,7 @@ var Driver = class {
|
|
|
5128
5369
|
* node or an edge that declares a `needs` asks, and a run that declares none never builds it.
|
|
5129
5370
|
*/
|
|
5130
5371
|
outputs() {
|
|
5131
|
-
const c = this.
|
|
5372
|
+
const c = this.cache();
|
|
5132
5373
|
for (; c.at < this.log.length; c.at += 1) {
|
|
5133
5374
|
const r = this.log[c.at];
|
|
5134
5375
|
let t = c.tasks.get(r.taskId);
|
|
@@ -5148,6 +5389,23 @@ var Driver = class {
|
|
|
5148
5389
|
}
|
|
5149
5390
|
return c.byNode;
|
|
5150
5391
|
}
|
|
5392
|
+
/**
|
|
5393
|
+
* The projection, made empty on first need and NOT caught up — `outputs()` catches it up; a `"$payload"` read, which
|
|
5394
|
+
* reads no output, only needs somewhere to keep its shared copy (W202).
|
|
5395
|
+
*/
|
|
5396
|
+
cache() {
|
|
5397
|
+
return this.outputsCache ??= {
|
|
5398
|
+
at: 0,
|
|
5399
|
+
byNode: /* @__PURE__ */ new Map(),
|
|
5400
|
+
tasks: /* @__PURE__ */ new Map(),
|
|
5401
|
+
orders: /* @__PURE__ */ new Map(),
|
|
5402
|
+
index: /* @__PURE__ */ new Map(),
|
|
5403
|
+
depths: /* @__PURE__ */ new Map(),
|
|
5404
|
+
examined: 0,
|
|
5405
|
+
payload: null,
|
|
5406
|
+
copies: 0
|
|
5407
|
+
};
|
|
5408
|
+
}
|
|
5151
5409
|
/**
|
|
5152
5410
|
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
5153
5411
|
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
@@ -5162,6 +5420,10 @@ var Driver = class {
|
|
|
5162
5420
|
* that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
|
|
5163
5421
|
* so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
|
|
5164
5422
|
* none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
|
|
5423
|
+
*
|
|
5424
|
+
* **Both a replacement and a removal let go of the entry OBJECT, and with it the entry's shared copy** (W202, ADR 0126,
|
|
5425
|
+
* `OutputEntry.shared`): a replaced output's next read makes a new copy of the new result, and a removed one's copy is
|
|
5426
|
+
* held nowhere — `RN-R13`'s v0.33 term, released with what it belongs to (ADR 0046).
|
|
5165
5427
|
*/
|
|
5166
5428
|
place(c, t, eff) {
|
|
5167
5429
|
const list = c.byNode.get(t.nodeId);
|
|
@@ -5171,6 +5433,7 @@ var Driver = class {
|
|
|
5171
5433
|
const at = list.indexOf(t.entry);
|
|
5172
5434
|
list.splice(at, 1);
|
|
5173
5435
|
orders.splice(at, 1);
|
|
5436
|
+
this.reindex(c, t.nodeId, t.entry, null);
|
|
5174
5437
|
t.entry = null;
|
|
5175
5438
|
if (list.length === 0) {
|
|
5176
5439
|
c.byNode.delete(t.nodeId);
|
|
@@ -5181,10 +5444,12 @@ var Driver = class {
|
|
|
5181
5444
|
const entry = { path: t.path, result: eff.result };
|
|
5182
5445
|
if (t.entry !== null && list !== void 0) {
|
|
5183
5446
|
list[list.indexOf(t.entry)] = entry;
|
|
5447
|
+
this.reindex(c, t.nodeId, t.entry, entry);
|
|
5184
5448
|
t.entry = entry;
|
|
5185
5449
|
return;
|
|
5186
5450
|
}
|
|
5187
5451
|
t.entry = entry;
|
|
5452
|
+
this.reindex(c, t.nodeId, null, entry);
|
|
5188
5453
|
if (list === void 0 || orders === void 0) {
|
|
5189
5454
|
c.byNode.set(t.nodeId, [entry]);
|
|
5190
5455
|
c.orders.set(t.nodeId, [t.order]);
|
|
@@ -5195,10 +5460,86 @@ var Driver = class {
|
|
|
5195
5460
|
list.splice(i, 0, entry);
|
|
5196
5461
|
orders.splice(i, 0, t.order);
|
|
5197
5462
|
}
|
|
5463
|
+
/**
|
|
5464
|
+
* **One entry's change carried into every index built for its node** — W199, ADR 0124: called by `place` at each of
|
|
5465
|
+
* its cases (a first entry, an append, an insertion at a first-record position, a replacement in place, a removal), so
|
|
5466
|
+
* the index is caught up exactly when `byNode` is and can say nothing `byNode` does not. `was` is the entry leaving
|
|
5467
|
+
* (`null` for an insertion), `now` the one arriving (`null` for a removal); a replacement keeps its path, so its
|
|
5468
|
+
* position. A group is ordered by PATH, not by first record, so where `byNode` inserted mid-list the group finds its own
|
|
5469
|
+
* place by a binary search. An entry with fewer than `depth` segments is visible to no reader at that depth and is filed
|
|
5470
|
+
* nowhere.
|
|
5471
|
+
*/
|
|
5472
|
+
reindex(c, nodeId, was, now) {
|
|
5473
|
+
const byDepth = c.index.get(nodeId);
|
|
5474
|
+
if (byDepth === void 0) return;
|
|
5475
|
+
const path = (now ?? was).path;
|
|
5476
|
+
for (const [depth, groups] of byDepth) {
|
|
5477
|
+
const key = visibilityKey(path, depth);
|
|
5478
|
+
if (key === null) continue;
|
|
5479
|
+
c.examined += 1;
|
|
5480
|
+
const group = groups.get(key);
|
|
5481
|
+
if (was !== null) {
|
|
5482
|
+
const at = groupPosition(group, path);
|
|
5483
|
+
if (now !== null) group[at] = now;
|
|
5484
|
+
else if (group.length === 1) groups.delete(key);
|
|
5485
|
+
else group.splice(at, 1);
|
|
5486
|
+
} else if (group === void 0) {
|
|
5487
|
+
groups.set(key, [now]);
|
|
5488
|
+
} else {
|
|
5489
|
+
group.splice(groupPosition(group, path), 0, now);
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
/**
|
|
5494
|
+
* **A node's outputs grouped for readers at `depth`** — built on the first read at that depth from the node's current
|
|
5495
|
+
* entries (each one examined), each group sorted ONCE, and from then on kept by `reindex`.
|
|
5496
|
+
*/
|
|
5497
|
+
groupsOf(c, nodeId, depth) {
|
|
5498
|
+
let byDepth = c.index.get(nodeId);
|
|
5499
|
+
if (byDepth === void 0) c.index.set(nodeId, byDepth = /* @__PURE__ */ new Map());
|
|
5500
|
+
let groups = byDepth.get(depth);
|
|
5501
|
+
if (groups !== void 0) return groups;
|
|
5502
|
+
groups = /* @__PURE__ */ new Map();
|
|
5503
|
+
for (const e of c.byNode.get(nodeId) ?? NO_OUTPUTS) {
|
|
5504
|
+
c.examined += 1;
|
|
5505
|
+
const key = visibilityKey(e.path, depth);
|
|
5506
|
+
if (key === null) continue;
|
|
5507
|
+
const group = groups.get(key);
|
|
5508
|
+
if (group === void 0) groups.set(key, [e]);
|
|
5509
|
+
else group.push(e);
|
|
5510
|
+
}
|
|
5511
|
+
for (const group of groups.values()) if (group.length > 1) group.sort((x, y) => compareBranchPaths(x.path, y.path));
|
|
5512
|
+
byDepth.set(depth, groups);
|
|
5513
|
+
return groups;
|
|
5514
|
+
}
|
|
5515
|
+
/** The depth `reader` and `target` share — `sharedRegionDepth`, a graph fact, asked once per pair. */
|
|
5516
|
+
depthOf(c, reader, target) {
|
|
5517
|
+
let byTarget = c.depths.get(reader);
|
|
5518
|
+
if (byTarget === void 0) c.depths.set(reader, byTarget = /* @__PURE__ */ new Map());
|
|
5519
|
+
let depth = byTarget.get(target);
|
|
5520
|
+
if (depth === void 0) byTarget.set(target, depth = sharedRegionDepth(this.regions, reader, target));
|
|
5521
|
+
return depth;
|
|
5522
|
+
}
|
|
5523
|
+
/**
|
|
5524
|
+
* **The outputs of `nodeId` visible to `atNodeId` at `here`, in `compareBranchPaths` order** — W199, ADR 0124: ADR
|
|
5525
|
+
* 0044's predicate answered by a LOOKUP. The predicate is `visibilityKey(here, d) === visibilityKey(path, d)` at the depth
|
|
5526
|
+
* `d` the two share (`outputIsVisible`), so the visible outputs are exactly the group filed under the reader's key: what
|
|
5527
|
+
* the filter over every output and the sort of its survivors returned until W199, found in the cost of its answer.
|
|
5528
|
+
* **The index's own list, never handed out** — the caller maps it to copies.
|
|
5529
|
+
*/
|
|
5530
|
+
visibleOutputs(atNodeId, here, nodeId) {
|
|
5531
|
+
this.outputs();
|
|
5532
|
+
const c = this.outputsCache;
|
|
5533
|
+
const depth = this.depthOf(c, atNodeId, nodeId);
|
|
5534
|
+
const key = visibilityKey(here, depth);
|
|
5535
|
+
if (key === null) return NO_OUTPUTS;
|
|
5536
|
+
return this.groupsOf(c, nodeId, depth).get(key) ?? NO_OUTPUTS;
|
|
5537
|
+
}
|
|
5198
5538
|
/**
|
|
5199
5539
|
* The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
|
|
5200
5540
|
* it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
|
|
5201
|
-
*
|
|
5541
|
+
* not a copy of them; since W202 an entry a reference has read also holds its ONE shared copy (`OutputEntry.shared`,
|
|
5542
|
+
* `RN-R13` v0.33), and `payload` the run's payload's — `tasks` is one slot per task id
|
|
5202
5543
|
* read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
|
|
5203
5544
|
* node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
|
|
5204
5545
|
* read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
|
|
@@ -5225,7 +5566,7 @@ var Driver = class {
|
|
|
5225
5566
|
compose(node, edgeValue, itemIndices, runPayload, item) {
|
|
5226
5567
|
const needs = node.needs;
|
|
5227
5568
|
if (!needs) return edgeValue;
|
|
5228
|
-
let
|
|
5569
|
+
let here = null;
|
|
5229
5570
|
const out = {};
|
|
5230
5571
|
for (const [key, ref] of Object.entries(needs)) {
|
|
5231
5572
|
const t = referenceTarget(ref);
|
|
@@ -5234,15 +5575,15 @@ var Driver = class {
|
|
|
5234
5575
|
continue;
|
|
5235
5576
|
}
|
|
5236
5577
|
if (t.form === "payload") {
|
|
5237
|
-
out[key] =
|
|
5578
|
+
out[key] = runPayload();
|
|
5238
5579
|
continue;
|
|
5239
5580
|
}
|
|
5240
5581
|
if (t.form === "item") {
|
|
5241
5582
|
out[key] = cloneValue(item());
|
|
5242
5583
|
continue;
|
|
5243
5584
|
}
|
|
5244
|
-
|
|
5245
|
-
const v = this.resolveReference(node.id,
|
|
5585
|
+
here ??= this.pathOf(node.id, itemIndices);
|
|
5586
|
+
const v = this.resolveReference(node.id, here, key, t, "needs");
|
|
5246
5587
|
if (v !== ABSENT) out[key] = v;
|
|
5247
5588
|
}
|
|
5248
5589
|
return out;
|
|
@@ -5253,7 +5594,7 @@ var Driver = class {
|
|
|
5253
5594
|
*
|
|
5254
5595
|
* **Extracted by [W42](../../doc/work-items/w42-what-a-decision-sees.md) rather
|
|
5255
5596
|
* than copied.** An EDGE's `needs` resolves by exactly these rules — same grammar,
|
|
5256
|
-
* same visibility predicate, same order, same
|
|
5597
|
+
* same visibility predicate, same order, same shared copy (since W202, ADR 0126) — and the ONE difference
|
|
5257
5598
|
* is that an edge has no `"."` form, which is a property of the caller rather than
|
|
5258
5599
|
* of this expression. *Two callers, one resolution, so a change to what a reference
|
|
5259
5600
|
* means cannot mean two different things to a worker and to a predicate*, which is
|
|
@@ -5263,8 +5604,8 @@ var Driver = class {
|
|
|
5263
5604
|
*
|
|
5264
5605
|
* @throws UnresolvedReference — `FD-R3`.
|
|
5265
5606
|
*/
|
|
5266
|
-
resolveReference(atNodeId, here,
|
|
5267
|
-
const visible =
|
|
5607
|
+
resolveReference(atNodeId, here, key, t, site) {
|
|
5608
|
+
const visible = this.visibleOutputs(atNodeId, here, t.node);
|
|
5268
5609
|
if (visible.length === 0 && t.conditional) {
|
|
5269
5610
|
return t.form === "all" ? [] : ABSENT;
|
|
5270
5611
|
}
|
|
@@ -5273,14 +5614,38 @@ var Driver = class {
|
|
|
5273
5614
|
`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.`
|
|
5274
5615
|
);
|
|
5275
5616
|
}
|
|
5276
|
-
|
|
5617
|
+
const c = this.outputsCache;
|
|
5618
|
+
c.examined += t.form === "all" ? visible.length : 1;
|
|
5619
|
+
return t.form === "all" ? visible.map((e) => this.sharedOf(c, e)) : this.sharedOf(c, visible[0]);
|
|
5620
|
+
}
|
|
5621
|
+
/** **An output entry's shared copy**, made at its first read and kept on the entry (`OutputEntry.shared`) — counted. */
|
|
5622
|
+
sharedOf(c, e) {
|
|
5623
|
+
if (!("shared" in e)) {
|
|
5624
|
+
e.shared = sharedCopy(e.result);
|
|
5625
|
+
c.copies += 1;
|
|
5626
|
+
}
|
|
5627
|
+
return e.shared;
|
|
5628
|
+
}
|
|
5629
|
+
/**
|
|
5630
|
+
* **`"$payload"`'s value in a run: the run's ONE shared copy** — W202, ADR 0126, P41 decision 2: `sharedCopy` of the
|
|
5631
|
+
* header's `entryPayload` (`runPayload()`), made at the first `"$payload"` read of the run — or of a resume, whose driver
|
|
5632
|
+
* is a new one — and handed to every reader at both sites. Counted once. `FD-R4` does not come here: it composes from
|
|
5633
|
+
* the caller's own value, before a header exists, and keeps its clone.
|
|
5634
|
+
*/
|
|
5635
|
+
sharedPayload() {
|
|
5636
|
+
const c = this.cache();
|
|
5637
|
+
if (c.payload === null) {
|
|
5638
|
+
c.payload = { value: sharedCopy(this.runPayload()) };
|
|
5639
|
+
c.copies += 1;
|
|
5640
|
+
}
|
|
5641
|
+
return c.payload.value;
|
|
5277
5642
|
}
|
|
5278
5643
|
/**
|
|
5279
5644
|
* **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
|
|
5280
5645
|
* caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
|
|
5281
5646
|
* reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
|
|
5282
|
-
* header exists. Read only where a node or an edge composes the token — a run declaring none never
|
|
5283
|
-
* (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
|
|
5647
|
+
* header exists. Read only where a node or an edge composes the token — a run declaring none never copies it
|
|
5648
|
+
* (P38 decision 2); since v0.47 read ONCE, by `sharedPayload`, whose one shared copy every reader is handed. The header is set before the first `enter` in both doors, so `null` here is not a run.
|
|
5284
5649
|
*/
|
|
5285
5650
|
runPayload() {
|
|
5286
5651
|
return this.header?.entryPayload;
|
|
@@ -5316,8 +5681,8 @@ var Driver = class {
|
|
|
5316
5681
|
* received anyway* is `ctx.outcome`, which it already has. So there is no
|
|
5317
5682
|
* `edgeValue` parameter here and none is derivable — see the call site. **A
|
|
5318
5683
|
* `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
|
|
5319
|
-
* the NODE received but read from the run's header
|
|
5320
|
-
* copy
|
|
5684
|
+
* the NODE received but read from the run's header — since v0.47 the run's ONE shared
|
|
5685
|
+
* copy of it, `sharedPayload()` above, the object a worker's `"$payload"` key is handed (ADR 0126).
|
|
5321
5686
|
*
|
|
5322
5687
|
* @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
|
|
5323
5688
|
*/
|
|
@@ -5325,12 +5690,12 @@ var Driver = class {
|
|
|
5325
5690
|
const needs = edge.needs;
|
|
5326
5691
|
if (!needs) return EMPTY_NEEDS;
|
|
5327
5692
|
const here = this.pathOf(node.id, itemIndices);
|
|
5328
|
-
|
|
5693
|
+
this.outputs();
|
|
5329
5694
|
const out = {};
|
|
5330
5695
|
for (const [key, ref] of Object.entries(needs)) {
|
|
5331
5696
|
const t = referenceTarget(ref);
|
|
5332
5697
|
if (t.form === "payload") {
|
|
5333
|
-
out[key] =
|
|
5698
|
+
out[key] = this.sharedPayload();
|
|
5334
5699
|
continue;
|
|
5335
5700
|
}
|
|
5336
5701
|
if (t.form === "item") {
|
|
@@ -5340,7 +5705,7 @@ var Driver = class {
|
|
|
5340
5705
|
if (t.form === "self") {
|
|
5341
5706
|
throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
|
|
5342
5707
|
}
|
|
5343
|
-
const v = this.resolveReference(node.id, here,
|
|
5708
|
+
const v = this.resolveReference(node.id, here, key, t, "edge needs");
|
|
5344
5709
|
if (v !== ABSENT) out[key] = v;
|
|
5345
5710
|
}
|
|
5346
5711
|
return Object.freeze(out);
|
|
@@ -5361,7 +5726,7 @@ var Driver = class {
|
|
|
5361
5726
|
}
|
|
5362
5727
|
this.dispatched.add(taskId);
|
|
5363
5728
|
if (this.replay === null) this.unhanded.add(taskId);
|
|
5364
|
-
this.append({ kind: "dispatched", taskId });
|
|
5729
|
+
const dispatchedAt = this.append({ kind: "dispatched", taskId });
|
|
5365
5730
|
const type = this.nodeTypes.get(node.type);
|
|
5366
5731
|
const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
|
|
5367
5732
|
const config = {
|
|
@@ -5373,7 +5738,11 @@ var Driver = class {
|
|
|
5373
5738
|
...node.workerGroups !== void 0 ? { workerGroups: node.workerGroups } : {}
|
|
5374
5739
|
};
|
|
5375
5740
|
if (this.replay !== null) {
|
|
5376
|
-
this.replay.
|
|
5741
|
+
const reading = this.replay.reading;
|
|
5742
|
+
if (reading === null) this.replay.held.set(taskId, config);
|
|
5743
|
+
else if (node.id === reading.nodeId) {
|
|
5744
|
+
this.offer(reading, { nodeId: node.id, kind: "task", branchPath: this.pathOf(node.id, itemIndices), handed: "envelope", value: config.payload }, dispatchedAt);
|
|
5745
|
+
}
|
|
5377
5746
|
return;
|
|
5378
5747
|
}
|
|
5379
5748
|
if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
|
|
@@ -5735,6 +6104,15 @@ var Driver = class {
|
|
|
5735
6104
|
if (replay !== null && seq < replay.stored.length) {
|
|
5736
6105
|
const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
|
|
5737
6106
|
if (!same) throw this.diverged(seq, "a different element there");
|
|
6107
|
+
const r = replay.reading;
|
|
6108
|
+
if (r !== null) {
|
|
6109
|
+
r.elements += 1;
|
|
6110
|
+
if (r.pending !== null && r.pending.at === seq) {
|
|
6111
|
+
const taken = r.pending.input;
|
|
6112
|
+
r.pending = null;
|
|
6113
|
+
this.take(r, taken);
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
5738
6116
|
}
|
|
5739
6117
|
if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
|
|
5740
6118
|
return seq;
|
|
@@ -5818,7 +6196,7 @@ var Driver = class {
|
|
|
5818
6196
|
return;
|
|
5819
6197
|
}
|
|
5820
6198
|
this.firedAdd(region.fanoutId, rp);
|
|
5821
|
-
this.append({
|
|
6199
|
+
const firedAt = this.append({
|
|
5822
6200
|
kind: "joinSettled",
|
|
5823
6201
|
regionId: region.fanoutId,
|
|
5824
6202
|
regionPath: rp,
|
|
@@ -5828,6 +6206,10 @@ var Driver = class {
|
|
|
5828
6206
|
if (this.shut()) return;
|
|
5829
6207
|
const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
|
|
5830
6208
|
this.releaseArrivals(region.fanoutId, rp);
|
|
6209
|
+
const reading = this.replay?.reading ?? null;
|
|
6210
|
+
if (reading !== null && region.joinId === reading.nodeId) {
|
|
6211
|
+
this.offer(reading, { nodeId: region.joinId, kind: "join", branchPath: rp, handed: "collection", value: collected }, firedAt);
|
|
6212
|
+
}
|
|
5831
6213
|
const join = this.graph.byId.get(region.joinId);
|
|
5832
6214
|
if (!join || join.kind !== "join") return;
|
|
5833
6215
|
const edge = join.edges?.[0];
|
|
@@ -5962,6 +6344,46 @@ var Driver = class {
|
|
|
5962
6344
|
return this.effectiveOutcomeOf(taskId)?.terminalState ?? null;
|
|
5963
6345
|
}
|
|
5964
6346
|
};
|
|
6347
|
+
function readTarget(flow, runId, target) {
|
|
6348
|
+
const unknown = (why) => ({ ok: false, reason: "unknown", index: null, message: `the target names nothing this run can have: ${why} (RN-R45)` });
|
|
6349
|
+
const t = target;
|
|
6350
|
+
const nodes = flow.nodes;
|
|
6351
|
+
if (typeof t?.taskId === "string") {
|
|
6352
|
+
let parts;
|
|
6353
|
+
try {
|
|
6354
|
+
parts = parseTaskId(t.taskId);
|
|
6355
|
+
} catch (e) {
|
|
6356
|
+
return unknown(`task id ${JSON.stringify(t.taskId)} does not parse \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
6357
|
+
}
|
|
6358
|
+
if (parts.runId !== runId) return unknown(`task id ${JSON.stringify(t.taskId)} is of run "${parts.runId}", and this run is "${runId}"`);
|
|
6359
|
+
if (parts.attempt !== 0) return unknown(`task id ${JSON.stringify(t.taskId)} carries attempt ${parts.attempt}, and a run mints attempt 0`);
|
|
6360
|
+
const node = nodes.find((n) => n.id === parts.nodeId);
|
|
6361
|
+
if (node === void 0 || node.kind !== "task" && node.kind !== "ask") {
|
|
6362
|
+
return unknown(`task id ${JSON.stringify(t.taskId)} names ${node === void 0 ? "no node of the definition" : `a ${node.kind}, which has no task id`}`);
|
|
6363
|
+
}
|
|
6364
|
+
return { nodeId: parts.nodeId, branchPath: parts.branchPath };
|
|
6365
|
+
}
|
|
6366
|
+
if (typeof t?.nodeId === "string") {
|
|
6367
|
+
const nodeId = t.nodeId;
|
|
6368
|
+
if (!nodes.some((n) => n.id === nodeId)) return unknown(`the definition has no node "${nodeId}"`);
|
|
6369
|
+
if (t.branchPath === void 0) return { nodeId, branchPath: null };
|
|
6370
|
+
const path = t.branchPath;
|
|
6371
|
+
let ok = typeof path === "string";
|
|
6372
|
+
if (ok) {
|
|
6373
|
+
try {
|
|
6374
|
+
parseTaskId(`${runId}:${nodeId}:${path}:0`);
|
|
6375
|
+
} catch {
|
|
6376
|
+
ok = false;
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
if (!ok) return unknown(`branch path ${JSON.stringify(path) ?? String(path)} is not spec/task-id.md's grammar ("-", or dot-separated non-negative integers)`);
|
|
6380
|
+
return { nodeId, branchPath: path };
|
|
6381
|
+
}
|
|
6382
|
+
return unknown("a target is { taskId } or { nodeId, branchPath? }");
|
|
6383
|
+
}
|
|
6384
|
+
function inputOf(flow, options, run, target) {
|
|
6385
|
+
return Driver.read(flow, options, run, target).answer;
|
|
6386
|
+
}
|
|
5965
6387
|
|
|
5966
6388
|
// src/fold/accumulator.ts
|
|
5967
6389
|
var Suspended = class {
|
|
@@ -6473,74 +6895,6 @@ function nodeOf(s, taskId) {
|
|
|
6473
6895
|
return s.nodes[taskId] ?? null;
|
|
6474
6896
|
}
|
|
6475
6897
|
|
|
6476
|
-
// src/fold/policies.ts
|
|
6477
|
-
var arrivalCount = {
|
|
6478
|
-
id: "arrival-count",
|
|
6479
|
-
// **`region` is deliberately unread.** It was the source of `need` and is now
|
|
6480
|
-
// the source of nothing: `branches` cannot answer *how many branches did this
|
|
6481
|
-
// region open* for a map, and a policy that read it for a fanout and counted for
|
|
6482
|
-
// a map would be branching on the node kind — which it cannot see
|
|
6483
|
-
// ([Q159](../../doc/questions/q159.md)) and, per that row, should not.
|
|
6484
|
-
open: () => ({ need: 0, arrived: [] }),
|
|
6485
|
-
step: (s, e) => {
|
|
6486
|
-
if (e.type === "branch.opened") return { ...s, need: s.need + 1 };
|
|
6487
|
-
if (e.type !== "branch.arrived" || s.arrived.includes(e.branchPath)) return s;
|
|
6488
|
-
return { ...s, arrived: [...s.arrived, e.branchPath] };
|
|
6489
|
-
},
|
|
6490
|
-
// **`s.need > 0` is a PIN, not a refinement**, and it is exactly `JP-R8`'s own
|
|
6491
|
-
// *"having been positive"* argument in the policy that argument was denied to.
|
|
6492
|
-
// A bare `arrived.length >= need` is true at `0 >= 0`, so without it `open`
|
|
6493
|
-
// itself reports `satisfied` — and at a map of zero items this policy would fire
|
|
6494
|
-
// a join **no branch ever reached**, which is character for character what
|
|
6495
|
-
// [ADR 0025](../../doc/decisions/0025-the-zero-arrival-join-is-reportable-and-arrival-counting-is-not-repaired.md)
|
|
6496
|
-
// REFUSED `JP-R7`'s variant for. It is vacuous on every `fanout` region, because
|
|
6497
|
-
// `FD-V9`'s floor puts `need` at 2 or more.
|
|
6498
|
-
status: (s) => s.need > 0 && s.arrived.length >= s.need ? "satisfied" : "waiting",
|
|
6499
|
-
// Never `unsatisfiable`: this policy has no way to know a join can never fire,
|
|
6500
|
-
// which is precisely the argument Q4 states as a testable claim. **At a map of
|
|
6501
|
-
// zero items it says `waiting`, exactly as the other two do** — the region is
|
|
6502
|
-
// reported dead by the FOLD (`RN-R10`, via the `regionEntered` input) rather
|
|
6503
|
-
// than by any policy, which is `spec/join-policy.md` section 5's own case.
|
|
6504
|
-
describe: (s) => ({ need: s.need, arrived: [...s.arrived] })
|
|
6505
|
-
};
|
|
6506
|
-
var liveToken = {
|
|
6507
|
-
id: "live-token",
|
|
6508
|
-
open: () => ({ live: 0, positive: false }),
|
|
6509
|
-
step: (s, e) => {
|
|
6510
|
-
if (e.type === "branch.opened") return { live: s.live + 1, positive: true };
|
|
6511
|
-
if (e.type === "branch.arrived" || e.type === "branch.exited" || e.type === "branch.lost") {
|
|
6512
|
-
return { ...s, live: s.live - 1 };
|
|
6513
|
-
}
|
|
6514
|
-
return s;
|
|
6515
|
-
},
|
|
6516
|
-
status: (s) => s.positive && s.live === 0 ? "satisfied" : "waiting",
|
|
6517
|
-
// NOT requiring an arrival before reporting `satisfied`, and NOT returning
|
|
6518
|
-
// `unsatisfiable` on zero-with-no-arrivals. That is JP-R8's third policy, and
|
|
6519
|
-
// it is the one that would hand live-token a graceful answer arrival-count is
|
|
6520
|
-
// structurally denied — which is the asymmetry JP-R8 exists to forbid.
|
|
6521
|
-
describe: (s) => ({ live: s.live, positive: s.positive })
|
|
6522
|
-
};
|
|
6523
|
-
var liveTokenRequiringArrival = {
|
|
6524
|
-
id: "live-token-requiring-arrival",
|
|
6525
|
-
open: () => ({ live: 0, positive: false, arrived: 0 }),
|
|
6526
|
-
step: (s, e) => {
|
|
6527
|
-
if (e.type === "branch.opened") return { ...s, live: s.live + 1, positive: true };
|
|
6528
|
-
if (e.type === "branch.arrived") return { ...s, live: s.live - 1, arrived: s.arrived + 1 };
|
|
6529
|
-
if (e.type === "branch.exited" || e.type === "branch.lost") return { ...s, live: s.live - 1 };
|
|
6530
|
-
return s;
|
|
6531
|
-
},
|
|
6532
|
-
status: (s) => {
|
|
6533
|
-
if (!s.positive || s.live !== 0) return "waiting";
|
|
6534
|
-
return s.arrived > 0 ? "satisfied" : "unsatisfiable";
|
|
6535
|
-
},
|
|
6536
|
-
describe: (s) => ({ live: s.live, positive: s.positive, arrived: s.arrived })
|
|
6537
|
-
};
|
|
6538
|
-
var POLICIES = {
|
|
6539
|
-
"arrival-count": arrivalCount,
|
|
6540
|
-
"live-token": liveToken,
|
|
6541
|
-
"live-token-requiring-arrival": liveTokenRequiringArrival
|
|
6542
|
-
};
|
|
6543
|
-
|
|
6544
6898
|
export {
|
|
6545
6899
|
PREDICATE_OPS,
|
|
6546
6900
|
PREDICATE_ROOTS,
|
|
@@ -6556,6 +6910,7 @@ export {
|
|
|
6556
6910
|
innermostMapOf,
|
|
6557
6911
|
sharedRegionDepth,
|
|
6558
6912
|
outputIsVisible,
|
|
6913
|
+
visibilityKey,
|
|
6559
6914
|
branchIndexOf,
|
|
6560
6915
|
certainlySettled,
|
|
6561
6916
|
SCHEMA_KEYWORDS,
|
|
@@ -6661,6 +7016,10 @@ export {
|
|
|
6661
7016
|
selectEdge,
|
|
6662
7017
|
TERMINAL_EVENTS,
|
|
6663
7018
|
isTerminalEvent,
|
|
7019
|
+
arrivalCount,
|
|
7020
|
+
liveToken,
|
|
7021
|
+
liveTokenRequiringArrival,
|
|
7022
|
+
POLICIES,
|
|
6664
7023
|
InvalidDefinition,
|
|
6665
7024
|
DuplicateTaskId,
|
|
6666
7025
|
UnresolvedMapArity,
|
|
@@ -6676,15 +7035,12 @@ export {
|
|
|
6676
7035
|
UndispatchedTask,
|
|
6677
7036
|
MissingJoinPolicy,
|
|
6678
7037
|
Driver,
|
|
7038
|
+
inputOf,
|
|
6679
7039
|
Accumulator,
|
|
6680
7040
|
emptyState,
|
|
6681
7041
|
step,
|
|
6682
7042
|
fold,
|
|
6683
7043
|
foldLog,
|
|
6684
7044
|
isTerminal,
|
|
6685
|
-
nodeOf
|
|
6686
|
-
arrivalCount,
|
|
6687
|
-
liveToken,
|
|
6688
|
-
liveTokenRequiringArrival,
|
|
6689
|
-
POLICIES
|
|
7045
|
+
nodeOf
|
|
6690
7046
|
};
|