@particle-academy/fancy-flow 0.72.0 → 0.73.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/README.md +22 -0
- package/dist/{chunk-57QTX3S3.js → chunk-3RXQGYGH.js} +3 -3
- package/dist/{chunk-57QTX3S3.js.map → chunk-3RXQGYGH.js.map} +1 -1
- package/dist/{chunk-RQP666FB.js → chunk-7DJ56PUA.js} +4 -4
- package/dist/{chunk-RQP666FB.js.map → chunk-7DJ56PUA.js.map} +1 -1
- package/dist/{chunk-C5K2DSZB.js → chunk-EX34GCKK.js} +7 -5
- package/dist/chunk-EX34GCKK.js.map +1 -0
- package/dist/{chunk-ZEDBF2V6.js → chunk-GQ5LEA7Q.js} +3 -3
- package/dist/{chunk-ZEDBF2V6.js.map → chunk-GQ5LEA7Q.js.map} +1 -1
- package/dist/{chunk-E3U3MBKI.js → chunk-K7KO3UJ3.js} +3 -3
- package/dist/{chunk-E3U3MBKI.js.map → chunk-K7KO3UJ3.js.map} +1 -1
- package/dist/{chunk-R3GQ72WA.js → chunk-LYYQO7EY.js} +4 -4
- package/dist/{chunk-R3GQ72WA.js.map → chunk-LYYQO7EY.js.map} +1 -1
- package/dist/{chunk-6TC44VCY.js → chunk-VAF2SWTC.js} +3 -3
- package/dist/{chunk-6TC44VCY.js.map → chunk-VAF2SWTC.js.map} +1 -1
- package/dist/durable/index.d.cts +138 -13
- package/dist/durable/index.d.ts +138 -13
- package/dist/durable.cjs +75 -13
- package/dist/durable.cjs.map +1 -1
- package/dist/durable.js +69 -12
- package/dist/durable.js.map +1 -1
- package/dist/engine.cjs +5 -3
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +2 -2
- package/dist/engine.d.ts +2 -2
- package/dist/engine.js +4 -4
- package/dist/index.cjs +5 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -12
- package/dist/registry.cjs +5 -3
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +2 -2
- package/dist/{run-cohort-DL9JzzPU.d.cts → run-cohort-DNpPa0mq.d.cts} +1 -1
- package/dist/{run-cohort-CjUBWg6X.d.ts → run-cohort-rgT_FX3V.d.ts} +1 -1
- package/dist/{run-flow-CxEGBOxd.d.ts → run-flow-BLP9rXfO.d.ts} +22 -1
- package/dist/{run-flow-B_8hgO5_.d.cts → run-flow-pW0PllaZ.d.cts} +22 -1
- package/dist/runtime/index.d.cts +3 -3
- package/dist/runtime/index.d.ts +3 -3
- package/dist/runtime.cjs +5 -3
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +3 -3
- package/dist/schema.cjs +5 -3
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +3 -3
- package/dist/screens.cjs +5 -3
- package/dist/screens.cjs.map +1 -1
- package/dist/screens.js +4 -4
- package/dist/ux.cjs +5 -3
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-C5K2DSZB.js.map +0 -1
package/dist/durable.cjs
CHANGED
|
@@ -72,6 +72,10 @@ var InMemoryClaimStore = class {
|
|
|
72
72
|
/**
|
|
73
73
|
* Drop a paused node's claim so a recorded answer can re-run it.
|
|
74
74
|
*
|
|
75
|
+
* A PAUSED row holds one of the run's dispatch slots (see `selectDispatch`),
|
|
76
|
+
* so this is also what frees the slot: until the row is released, a serial
|
|
77
|
+
* run's `advance()` hands out nothing, the gate included.
|
|
78
|
+
*
|
|
75
79
|
* Not part of the interface: resuming a human gate is the host's decision and
|
|
76
80
|
* its storage's business. Provided here because the in-memory store is also
|
|
77
81
|
* what the tests resume through.
|
|
@@ -720,7 +724,7 @@ function resolveWorkflowProps(declared, passed) {
|
|
|
720
724
|
// src/runtime/run-flow.ts
|
|
721
725
|
async function runFlow(graph, executors, onEvent = () => {
|
|
722
726
|
}, options = {}) {
|
|
723
|
-
const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes } = options;
|
|
727
|
+
const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes, nodeExecutors } = options;
|
|
724
728
|
const run = options.run === void 0 ? void 0 : RunIdentity.from(options.run);
|
|
725
729
|
const outputs = {};
|
|
726
730
|
const portValues = /* @__PURE__ */ new Map();
|
|
@@ -808,7 +812,7 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
808
812
|
onEvent({ type: "node-status", nodeId: node.id, status: "running" });
|
|
809
813
|
announce(onEvent, node, "start");
|
|
810
814
|
const inputs = collectInputs(node, incoming, portValues, initialInputs, props, declaresProps);
|
|
811
|
-
const exec = pickExecutor(executors, node);
|
|
815
|
+
const exec = pickExecutor(executors, node, nodeExecutors);
|
|
812
816
|
if (!exec) {
|
|
813
817
|
const tried = executorLookupIds(node);
|
|
814
818
|
const msg = `No executor registered for kind=${node.type} \u2014 tried ${tried.map((id) => `"${id}"`).join(", ")}. Key your registry by one of those.`;
|
|
@@ -928,7 +932,9 @@ function executorLookupIds(node) {
|
|
|
928
932
|
ids.push("*");
|
|
929
933
|
return [...new Set(ids)];
|
|
930
934
|
}
|
|
931
|
-
function pickExecutor(executors, node) {
|
|
935
|
+
function pickExecutor(executors, node, nodeExecutors) {
|
|
936
|
+
const pinned = nodeExecutors && Object.prototype.hasOwnProperty.call(nodeExecutors, node.id) ? nodeExecutors[node.id] : void 0;
|
|
937
|
+
if (pinned) return pinned;
|
|
932
938
|
for (const id of executorLookupIds(node)) {
|
|
933
939
|
if (executors[id]) return executors[id];
|
|
934
940
|
}
|
|
@@ -2732,14 +2738,43 @@ var Frontier = {
|
|
|
2732
2738
|
}
|
|
2733
2739
|
};
|
|
2734
2740
|
|
|
2741
|
+
// src/durable/dispatch.ts
|
|
2742
|
+
var UNLIMITED_CONCURRENCY = 0;
|
|
2743
|
+
var DEFAULT_MAX_CONCURRENT = 1;
|
|
2744
|
+
function assertMaxConcurrent(value) {
|
|
2745
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
2746
|
+
throw new RangeError(
|
|
2747
|
+
`maxConcurrent must be a positive integer, or UNLIMITED_CONCURRENCY (0) for the whole ready frontier, or unset for serial; got ${describe(value)}.`
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
function selectDispatch(ready, state, maxConcurrent) {
|
|
2751
|
+
const limit = assertMaxConcurrent(maxConcurrent);
|
|
2752
|
+
if (limit === UNLIMITED_CONCURRENCY) return [...ready];
|
|
2753
|
+
let held = 0;
|
|
2754
|
+
for (const entry of Object.values(state)) {
|
|
2755
|
+
if (entry.status === NodeRunStatus.CLAIMED || entry.status === NodeRunStatus.PAUSED) held++;
|
|
2756
|
+
}
|
|
2757
|
+
return ready.slice(0, Math.max(0, limit - held));
|
|
2758
|
+
}
|
|
2759
|
+
function describe(value) {
|
|
2760
|
+
if (typeof value === "number") return String(value);
|
|
2761
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2762
|
+
if (value === null) return "null";
|
|
2763
|
+
return typeof value;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2735
2766
|
// src/durable/replay.ts
|
|
2736
2767
|
var BOUNDARY = "fancy-flow:node-boundary";
|
|
2768
|
+
var FENCE_PORT = "fancy-flow:fenced";
|
|
2737
2769
|
async function replayUpTo(graph, nodeId, executors, options = {}) {
|
|
2738
|
-
const
|
|
2770
|
+
const fencedOff = /* @__PURE__ */ new Set();
|
|
2771
|
+
const fence = (ctx) => {
|
|
2772
|
+
fencedOff.add(ctx.node.id);
|
|
2773
|
+
return { __port: FENCE_PORT, value: null };
|
|
2774
|
+
};
|
|
2775
|
+
const fences = {};
|
|
2739
2776
|
for (const node of graph.nodes) {
|
|
2740
|
-
if (node.id !== nodeId)
|
|
2741
|
-
fenced[node.id] = (ctx) => ctx.abort(BOUNDARY);
|
|
2742
|
-
}
|
|
2777
|
+
if (node.id !== nodeId) fences[node.id] = fence;
|
|
2743
2778
|
}
|
|
2744
2779
|
const ports = {};
|
|
2745
2780
|
const collect = (event) => {
|
|
@@ -2749,12 +2784,17 @@ async function replayUpTo(graph, nodeId, executors, options = {}) {
|
|
|
2749
2784
|
}
|
|
2750
2785
|
options.onEvent?.(event);
|
|
2751
2786
|
};
|
|
2752
|
-
const result = await runFlow(graph,
|
|
2787
|
+
const result = await runFlow(graph, executors, collect, {
|
|
2753
2788
|
initialInputs: options.initialInputs ?? {},
|
|
2754
2789
|
resumeOutputs: options.resumeOutputs ?? {},
|
|
2755
2790
|
depth: options.depth ?? 0,
|
|
2756
|
-
run: options.run
|
|
2791
|
+
run: options.run,
|
|
2792
|
+
nodeExecutors: fences
|
|
2757
2793
|
});
|
|
2794
|
+
for (const id of fencedOff) {
|
|
2795
|
+
delete result.outputs[id];
|
|
2796
|
+
delete ports[id];
|
|
2797
|
+
}
|
|
2758
2798
|
return {
|
|
2759
2799
|
result,
|
|
2760
2800
|
ports,
|
|
@@ -2922,6 +2962,7 @@ var Coordinator = class {
|
|
|
2922
2962
|
this.store = options.store ?? new InMemoryClaimStore();
|
|
2923
2963
|
this.initialInputs = options.initialInputs ?? {};
|
|
2924
2964
|
this.retry = options.retry ?? new RetryPolicy();
|
|
2965
|
+
this.maxConcurrent = assertMaxConcurrent(options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT);
|
|
2925
2966
|
this.onEvent = options.onEvent;
|
|
2926
2967
|
}
|
|
2927
2968
|
get runKey() {
|
|
@@ -2931,6 +2972,13 @@ var Coordinator = class {
|
|
|
2931
2972
|
/**
|
|
2932
2973
|
* Which nodes may be dispatched right now.
|
|
2933
2974
|
*
|
|
2975
|
+
* The ready frontier, cut to the run's {@link maxConcurrent} budget: the
|
|
2976
|
+
* first `maxConcurrent - held` ready ids in declaration order, where `held`
|
|
2977
|
+
* counts CLAIMED and PAUSED rows. Under the serial default that is at most one
|
|
2978
|
+
* id, and none while a node is still running or a gate is waiting on a person.
|
|
2979
|
+
* An empty result with work held is a throttled run, not a stalled one: the
|
|
2980
|
+
* held node's settle is what calls this again.
|
|
2981
|
+
*
|
|
2934
2982
|
* Also settles the skip cascade, because a skip is a decision the frontier
|
|
2935
2983
|
* just made and a second caller must not make it again.
|
|
2936
2984
|
*
|
|
@@ -2946,7 +2994,12 @@ var Coordinator = class {
|
|
|
2946
2994
|
const frontier = Frontier.compute(this.graph, state);
|
|
2947
2995
|
const settled = await Frontier.settleSkips(this.store, this.runKey, frontier.skipped);
|
|
2948
2996
|
this.warnForSkipped(settled, state);
|
|
2949
|
-
|
|
2997
|
+
const after = { ...state };
|
|
2998
|
+
for (const nodeId of frontier.skipped) {
|
|
2999
|
+
const row = state[nodeId];
|
|
3000
|
+
after[nodeId] = row ? { ...row, status: NodeRunStatus.SKIPPED, ports: [] } : { status: NodeRunStatus.SKIPPED, ports: [], attempts: 0, firstAttemptAt: "" };
|
|
3001
|
+
}
|
|
3002
|
+
return selectDispatch(frontier.ready, after, this.maxConcurrent);
|
|
2950
3003
|
}
|
|
2951
3004
|
/**
|
|
2952
3005
|
* Claim, execute and checkpoint one node.
|
|
@@ -2985,12 +3038,12 @@ var Coordinator = class {
|
|
|
2985
3038
|
await this.store.pause(this.runKey, nodeId, result.error ?? "");
|
|
2986
3039
|
return { nodeId, status: "paused", pause, claimed: true, attempt };
|
|
2987
3040
|
}
|
|
2988
|
-
if (isBoundary(result.error)) {
|
|
3041
|
+
if (isBoundary(result.error) || result.ok) {
|
|
2989
3042
|
await this.store.skip(this.runKey, nodeId);
|
|
2990
3043
|
return {
|
|
2991
3044
|
nodeId,
|
|
2992
3045
|
status: "skipped",
|
|
2993
|
-
error: "
|
|
3046
|
+
error: "the engine found no live inbound edge for this node",
|
|
2994
3047
|
claimed: true,
|
|
2995
3048
|
attempt
|
|
2996
3049
|
};
|
|
@@ -3008,6 +3061,12 @@ var Coordinator = class {
|
|
|
3008
3061
|
/**
|
|
3009
3062
|
* Drive the graph here, in this process, one node at a time.
|
|
3010
3063
|
*
|
|
3064
|
+
* It asks `advance()` exactly as a queue adapter does, so it runs nodes in the
|
|
3065
|
+
* order a queued run under the same `maxConcurrent` would dispatch them. A run
|
|
3066
|
+
* that finishes produces the same outputs under every limit. A run that stops
|
|
3067
|
+
* on a pause or a failure can have run a different set of nodes before it
|
|
3068
|
+
* stopped, because the order among nodes that are ready together differs.
|
|
3069
|
+
*
|
|
3011
3070
|
* Every checkpoint is written exactly as a queued run writes it, so a crash
|
|
3012
3071
|
* mid-loop resumes from the same place a crashed worker would.
|
|
3013
3072
|
*
|
|
@@ -3016,7 +3075,7 @@ var Coordinator = class {
|
|
|
3016
3075
|
* claim with the same owner token — so the step key it derives is unchanged,
|
|
3017
3076
|
* which is what makes the retry idempotent rather than duplicative.
|
|
3018
3077
|
*/
|
|
3019
|
-
async runToCompletion(maxPasses = 1e4) {
|
|
3078
|
+
async runToCompletion(maxPasses = Math.max(1e4, this.graph.nodes.length + 1)) {
|
|
3020
3079
|
for (let pass = 0; pass < maxPasses; pass++) {
|
|
3021
3080
|
const ready = await this.advance();
|
|
3022
3081
|
if (ready.length === 0) break;
|
|
@@ -3166,6 +3225,7 @@ function cryptoRandom() {
|
|
|
3166
3225
|
|
|
3167
3226
|
exports.BOUNDARY = BOUNDARY;
|
|
3168
3227
|
exports.Coordinator = Coordinator;
|
|
3228
|
+
exports.FENCE_PORT = FENCE_PORT;
|
|
3169
3229
|
exports.Frontier = Frontier;
|
|
3170
3230
|
exports.InMemoryClaimStore = InMemoryClaimStore;
|
|
3171
3231
|
exports.NodeRunStatus = NodeRunStatus;
|
|
@@ -3173,11 +3233,13 @@ exports.NotAwaitingHuman = NotAwaitingHuman;
|
|
|
3173
3233
|
exports.RetryPolicy = RetryPolicy;
|
|
3174
3234
|
exports.SETTLED = SETTLED;
|
|
3175
3235
|
exports.Submissions = Submissions;
|
|
3236
|
+
exports.UNLIMITED_CONCURRENCY = UNLIMITED_CONCURRENCY;
|
|
3176
3237
|
exports.UNSAFE_TO_REPLAY = UNSAFE_TO_REPLAY;
|
|
3177
3238
|
exports.durableApproval = durableApproval;
|
|
3178
3239
|
exports.durableUserInput = durableUserInput;
|
|
3179
3240
|
exports.isBoundary = isBoundary;
|
|
3180
3241
|
exports.isSettled = isSettled;
|
|
3181
3242
|
exports.replayUpTo = replayUpTo;
|
|
3243
|
+
exports.selectDispatch = selectDispatch;
|
|
3182
3244
|
//# sourceMappingURL=durable.cjs.map
|
|
3183
3245
|
//# sourceMappingURL=durable.cjs.map
|