@particle-academy/fancy-flow 0.28.0 → 0.29.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 +102 -15
- package/dist/{chunk-HHFOHHAQ.js → chunk-EBE3FEAJ.js} +2 -2
- package/dist/{chunk-HHFOHHAQ.js.map → chunk-EBE3FEAJ.js.map} +1 -1
- package/dist/chunk-FKOXQP76.js +50 -0
- package/dist/chunk-FKOXQP76.js.map +1 -0
- package/dist/engine.cjs +46 -0
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/index.cjs +46 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/run-cohort-CV7EcwGG.d.ts +111 -0
- package/dist/run-cohort-DitsaYgS.d.cts +111 -0
- package/dist/runtime/index.d.cts +2 -2
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime.cjs +46 -0
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +2 -1
- package/package.json +1 -1
- package/dist/run-flow-9U-gaYVF.d.ts +0 -35
- package/dist/run-flow-AXzOR-Wq.d.cts +0 -35
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { F as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-CMSrWVYM.js';
|
|
2
|
+
|
|
3
|
+
type RunOptions = {
|
|
4
|
+
/** Stop the run after this many ms. Default: no timeout. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Abort signal — host can cancel the run. */
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
/** Initial inputs supplied to entry-point nodes (no incoming edges). */
|
|
9
|
+
initialInputs?: Record<string, Record<string, unknown>>;
|
|
10
|
+
/** Nesting depth — set by `subflow` when it runs a child graph. */
|
|
11
|
+
depth?: number;
|
|
12
|
+
};
|
|
13
|
+
type RunResult = {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** Outputs collected per node, keyed by node id. */
|
|
16
|
+
outputs: Record<string, unknown>;
|
|
17
|
+
/** Error captured if any node threw. */
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* runFlow — topological execution of a FlowGraph against an ExecutorRegistry.
|
|
22
|
+
*
|
|
23
|
+
* Each node runs once, when all upstream nodes have produced outputs on the
|
|
24
|
+
* connected ports. Decision nodes (or any executor that returns `{ branch:
|
|
25
|
+
* 'true' }`) can short-circuit specific output ports — only edges leaving
|
|
26
|
+
* an "active" port propagate to downstream nodes.
|
|
27
|
+
*
|
|
28
|
+
* Cycles are detected and abort the run with an error.
|
|
29
|
+
*
|
|
30
|
+
* The `onEvent` callback receives a stream of `RunEvent`s — wire it to a
|
|
31
|
+
* status feed, log panel, or store.
|
|
32
|
+
*/
|
|
33
|
+
declare function runFlow(graph: FlowGraph, executors: ExecutorRegistry, onEvent?: (event: RunEvent) => void, options?: RunOptions): Promise<RunResult>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How the flows one trigger fired should treat each other.
|
|
37
|
+
*
|
|
38
|
+
* - `serial-guarded` (default) — run in the declared order, one at a time,
|
|
39
|
+
* re-checking `guard` immediately before each. This is the safe default
|
|
40
|
+
* because it is the only one that notices when an earlier flow invalidated
|
|
41
|
+
* the state a later one was fired for.
|
|
42
|
+
* - `serial` — ordered, one at a time, unguarded.
|
|
43
|
+
* - `parallel` — all at once. Ordering is incidental and collisions are yours to
|
|
44
|
+
* handle; correct for fan-outs that share no state, and only those.
|
|
45
|
+
*/
|
|
46
|
+
type CohortPolicy = "serial-guarded" | "serial" | "parallel";
|
|
47
|
+
/**
|
|
48
|
+
* The precondition re-checked just before each flow starts.
|
|
49
|
+
*
|
|
50
|
+
* Return `false` (or throw) to skip that flow. The engine cannot know what
|
|
51
|
+
* "still valid" means for your data, so it asks you — right before the run,
|
|
52
|
+
* not at dispatch, because the whole hazard is what changed in between.
|
|
53
|
+
*/
|
|
54
|
+
type CohortGuard = (flow: FlowGraph, index: number) => boolean | Promise<boolean>;
|
|
55
|
+
type CohortOptions = RunOptions & {
|
|
56
|
+
policy?: CohortPolicy;
|
|
57
|
+
guard?: CohortGuard;
|
|
58
|
+
/** Why the guard said no — recorded on the skipped result. */
|
|
59
|
+
reason?: (flow: FlowGraph, index: number) => string;
|
|
60
|
+
};
|
|
61
|
+
type CohortResult = RunResult & {
|
|
62
|
+
index: number;
|
|
63
|
+
/** True when the flow never ran because its guard did not pass. */
|
|
64
|
+
skipped?: boolean;
|
|
65
|
+
skippedReason?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* runCohort — run every flow that one trigger event fired, as a group.
|
|
69
|
+
*
|
|
70
|
+
* ## Why this exists
|
|
71
|
+
*
|
|
72
|
+
* `runFlow` runs one graph. A host that fans a single webhook, schedule, or
|
|
73
|
+
* record change out to several flows usually loops it — and a loop, or worse a
|
|
74
|
+
* `Promise.all`, has no answer for the case that actually bites: one of those
|
|
75
|
+
* flows deletes or mutates the record they were ALL fired for. The others then
|
|
76
|
+
* run against state that is no longer there and resolve `ok: true`, having done
|
|
77
|
+
* nothing. Nothing throws. Nothing is logged. That silent success is the failure
|
|
78
|
+
* mode this function exists to remove.
|
|
79
|
+
*
|
|
80
|
+
* ## What it does
|
|
81
|
+
*
|
|
82
|
+
* Runs the flows in the order you declared, one at a time, and calls `guard`
|
|
83
|
+
* immediately before each. A flow whose guard does not pass is returned with
|
|
84
|
+
* `skipped: true` and a reason instead of being run — and the cohort carries on
|
|
85
|
+
* to the next one.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const results = await runCohort([enrich, archive, notify], executors, undefined, {
|
|
89
|
+
* initialInputs: { t: { deal } }, // snapshotted once, shared by all
|
|
90
|
+
* guard: async () => Boolean(await findDeal(deal.id)),
|
|
91
|
+
* reason: () => `deal ${deal.id} no longer exists`,
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* If `archive` deletes the deal, `notify` comes back skipped with that reason
|
|
96
|
+
* rather than notifying about nothing.
|
|
97
|
+
*
|
|
98
|
+
* ## Failure does not cancel the cohort
|
|
99
|
+
*
|
|
100
|
+
* A flow that fails is reported and the next one still runs. "The flow before me
|
|
101
|
+
* threw" is not an answer to "is my input still there" — the guard is, and it
|
|
102
|
+
* gets asked either way.
|
|
103
|
+
*
|
|
104
|
+
* The Laravel twin (`FancyFlow::dispatchCohort()` in `fancy-flow-php`) is the
|
|
105
|
+
* same contract across a queue, where each run is durable and hands on to its
|
|
106
|
+
* successor when it settles. Same policies, same guard semantics, same
|
|
107
|
+
* fail-closed rule.
|
|
108
|
+
*/
|
|
109
|
+
declare function runCohort(flows: FlowGraph[], executors: ExecutorRegistry, onEvent?: (event: RunEvent, index: number) => void, options?: CohortOptions): Promise<CohortResult[]>;
|
|
110
|
+
|
|
111
|
+
export { type CohortGuard as C, type RunOptions as R, type CohortOptions as a, type CohortPolicy as b, type CohortResult as c, type RunResult as d, runFlow as e, runCohort as r };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { F as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-CMSrWVYM.cjs';
|
|
2
|
+
|
|
3
|
+
type RunOptions = {
|
|
4
|
+
/** Stop the run after this many ms. Default: no timeout. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Abort signal — host can cancel the run. */
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
/** Initial inputs supplied to entry-point nodes (no incoming edges). */
|
|
9
|
+
initialInputs?: Record<string, Record<string, unknown>>;
|
|
10
|
+
/** Nesting depth — set by `subflow` when it runs a child graph. */
|
|
11
|
+
depth?: number;
|
|
12
|
+
};
|
|
13
|
+
type RunResult = {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** Outputs collected per node, keyed by node id. */
|
|
16
|
+
outputs: Record<string, unknown>;
|
|
17
|
+
/** Error captured if any node threw. */
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* runFlow — topological execution of a FlowGraph against an ExecutorRegistry.
|
|
22
|
+
*
|
|
23
|
+
* Each node runs once, when all upstream nodes have produced outputs on the
|
|
24
|
+
* connected ports. Decision nodes (or any executor that returns `{ branch:
|
|
25
|
+
* 'true' }`) can short-circuit specific output ports — only edges leaving
|
|
26
|
+
* an "active" port propagate to downstream nodes.
|
|
27
|
+
*
|
|
28
|
+
* Cycles are detected and abort the run with an error.
|
|
29
|
+
*
|
|
30
|
+
* The `onEvent` callback receives a stream of `RunEvent`s — wire it to a
|
|
31
|
+
* status feed, log panel, or store.
|
|
32
|
+
*/
|
|
33
|
+
declare function runFlow(graph: FlowGraph, executors: ExecutorRegistry, onEvent?: (event: RunEvent) => void, options?: RunOptions): Promise<RunResult>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How the flows one trigger fired should treat each other.
|
|
37
|
+
*
|
|
38
|
+
* - `serial-guarded` (default) — run in the declared order, one at a time,
|
|
39
|
+
* re-checking `guard` immediately before each. This is the safe default
|
|
40
|
+
* because it is the only one that notices when an earlier flow invalidated
|
|
41
|
+
* the state a later one was fired for.
|
|
42
|
+
* - `serial` — ordered, one at a time, unguarded.
|
|
43
|
+
* - `parallel` — all at once. Ordering is incidental and collisions are yours to
|
|
44
|
+
* handle; correct for fan-outs that share no state, and only those.
|
|
45
|
+
*/
|
|
46
|
+
type CohortPolicy = "serial-guarded" | "serial" | "parallel";
|
|
47
|
+
/**
|
|
48
|
+
* The precondition re-checked just before each flow starts.
|
|
49
|
+
*
|
|
50
|
+
* Return `false` (or throw) to skip that flow. The engine cannot know what
|
|
51
|
+
* "still valid" means for your data, so it asks you — right before the run,
|
|
52
|
+
* not at dispatch, because the whole hazard is what changed in between.
|
|
53
|
+
*/
|
|
54
|
+
type CohortGuard = (flow: FlowGraph, index: number) => boolean | Promise<boolean>;
|
|
55
|
+
type CohortOptions = RunOptions & {
|
|
56
|
+
policy?: CohortPolicy;
|
|
57
|
+
guard?: CohortGuard;
|
|
58
|
+
/** Why the guard said no — recorded on the skipped result. */
|
|
59
|
+
reason?: (flow: FlowGraph, index: number) => string;
|
|
60
|
+
};
|
|
61
|
+
type CohortResult = RunResult & {
|
|
62
|
+
index: number;
|
|
63
|
+
/** True when the flow never ran because its guard did not pass. */
|
|
64
|
+
skipped?: boolean;
|
|
65
|
+
skippedReason?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* runCohort — run every flow that one trigger event fired, as a group.
|
|
69
|
+
*
|
|
70
|
+
* ## Why this exists
|
|
71
|
+
*
|
|
72
|
+
* `runFlow` runs one graph. A host that fans a single webhook, schedule, or
|
|
73
|
+
* record change out to several flows usually loops it — and a loop, or worse a
|
|
74
|
+
* `Promise.all`, has no answer for the case that actually bites: one of those
|
|
75
|
+
* flows deletes or mutates the record they were ALL fired for. The others then
|
|
76
|
+
* run against state that is no longer there and resolve `ok: true`, having done
|
|
77
|
+
* nothing. Nothing throws. Nothing is logged. That silent success is the failure
|
|
78
|
+
* mode this function exists to remove.
|
|
79
|
+
*
|
|
80
|
+
* ## What it does
|
|
81
|
+
*
|
|
82
|
+
* Runs the flows in the order you declared, one at a time, and calls `guard`
|
|
83
|
+
* immediately before each. A flow whose guard does not pass is returned with
|
|
84
|
+
* `skipped: true` and a reason instead of being run — and the cohort carries on
|
|
85
|
+
* to the next one.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const results = await runCohort([enrich, archive, notify], executors, undefined, {
|
|
89
|
+
* initialInputs: { t: { deal } }, // snapshotted once, shared by all
|
|
90
|
+
* guard: async () => Boolean(await findDeal(deal.id)),
|
|
91
|
+
* reason: () => `deal ${deal.id} no longer exists`,
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* If `archive` deletes the deal, `notify` comes back skipped with that reason
|
|
96
|
+
* rather than notifying about nothing.
|
|
97
|
+
*
|
|
98
|
+
* ## Failure does not cancel the cohort
|
|
99
|
+
*
|
|
100
|
+
* A flow that fails is reported and the next one still runs. "The flow before me
|
|
101
|
+
* threw" is not an answer to "is my input still there" — the guard is, and it
|
|
102
|
+
* gets asked either way.
|
|
103
|
+
*
|
|
104
|
+
* The Laravel twin (`FancyFlow::dispatchCohort()` in `fancy-flow-php`) is the
|
|
105
|
+
* same contract across a queue, where each run is durable and hands on to its
|
|
106
|
+
* successor when it settles. Same policies, same guard semantics, same
|
|
107
|
+
* fail-closed rule.
|
|
108
|
+
*/
|
|
109
|
+
declare function runCohort(flows: FlowGraph[], executors: ExecutorRegistry, onEvent?: (event: RunEvent, index: number) => void, options?: CohortOptions): Promise<CohortResult[]>;
|
|
110
|
+
|
|
111
|
+
export { type CohortGuard as C, type RunOptions as R, type CohortOptions as a, type CohortPolicy as b, type CohortResult as c, type RunResult as d, runFlow as e, runCohort as r };
|
package/dist/runtime/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { r as runFlow } from '../run-
|
|
1
|
+
import { d as RunResult, R as RunOptions } from '../run-cohort-DitsaYgS.cjs';
|
|
2
|
+
export { C as CohortGuard, a as CohortOptions, b as CohortPolicy, c as CohortResult, r as runCohort, e as runFlow } from '../run-cohort-DitsaYgS.cjs';
|
|
3
3
|
import { e as NodeRunStatus, F as FlowGraph, E as ExecutorRegistry, a as FlowNode, b as FlowEdge } from '../types-CMSrWVYM.cjs';
|
|
4
4
|
import { NodeChange, EdgeChange, Connection } from '@xyflow/react';
|
|
5
5
|
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { r as runFlow } from '../run-
|
|
1
|
+
import { d as RunResult, R as RunOptions } from '../run-cohort-CV7EcwGG.js';
|
|
2
|
+
export { C as CohortGuard, a as CohortOptions, b as CohortPolicy, c as CohortResult, r as runCohort, e as runFlow } from '../run-cohort-CV7EcwGG.js';
|
|
3
3
|
import { e as NodeRunStatus, F as FlowGraph, E as ExecutorRegistry, a as FlowNode, b as FlowEdge } from '../types-CMSrWVYM.js';
|
|
4
4
|
import { NodeChange, EdgeChange, Connection } from '@xyflow/react';
|
|
5
5
|
|
package/dist/runtime.cjs
CHANGED
|
@@ -198,6 +198,51 @@ function activatedPorts(node, result) {
|
|
|
198
198
|
const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
|
|
199
199
|
return { ports: declared?.length ? declared : ["out"], value: result };
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
// src/runtime/run-cohort.ts
|
|
203
|
+
async function runCohort(flows, executors, onEvent = () => {
|
|
204
|
+
}, options = {}) {
|
|
205
|
+
const { policy = "serial-guarded", guard, reason, ...runOptions } = options;
|
|
206
|
+
if (policy === "parallel") {
|
|
207
|
+
return Promise.all(
|
|
208
|
+
flows.map(async (flow, index2) => ({
|
|
209
|
+
...await runFlow(flow, executors, (e) => onEvent(e, index2), runOptions),
|
|
210
|
+
index: index2
|
|
211
|
+
}))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const results = [];
|
|
215
|
+
for (const [index2, flow] of flows.entries()) {
|
|
216
|
+
if (policy === "serial-guarded" && guard) {
|
|
217
|
+
const why = () => reason?.(flow, index2) ?? "trigger precondition no longer holds";
|
|
218
|
+
let passes;
|
|
219
|
+
try {
|
|
220
|
+
passes = await guard(flow, index2);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
results.push({
|
|
223
|
+
ok: false,
|
|
224
|
+
outputs: {},
|
|
225
|
+
index: index2,
|
|
226
|
+
skipped: true,
|
|
227
|
+
skippedReason: `guard could not be evaluated: ${errorText(error)}`
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!passes) {
|
|
232
|
+
results.push({ ok: false, outputs: {}, index: index2, skipped: true, skippedReason: why() });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
results.push({
|
|
237
|
+
...await runFlow(flow, executors, (e) => onEvent(e, index2), runOptions),
|
|
238
|
+
index: index2
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return results;
|
|
242
|
+
}
|
|
243
|
+
function errorText(error) {
|
|
244
|
+
return error instanceof Error ? error.message : String(error);
|
|
245
|
+
}
|
|
201
246
|
function useFlowRun({ maxFeed = 200 } = {}) {
|
|
202
247
|
const [statuses, setStatuses] = ReactExports.useState({});
|
|
203
248
|
const [statusText, setStatusText] = ReactExports.useState({});
|
|
@@ -9330,6 +9375,7 @@ function useFlowHistory(flow) {
|
|
|
9330
9375
|
exports.applyOutputsToNodes = applyOutputsToNodes;
|
|
9331
9376
|
exports.applyStatusesToNodes = applyStatusesToNodes;
|
|
9332
9377
|
exports.createHistory = createHistory;
|
|
9378
|
+
exports.runCohort = runCohort;
|
|
9333
9379
|
exports.runFlow = runFlow;
|
|
9334
9380
|
exports.useFlowHistory = useFlowHistory;
|
|
9335
9381
|
exports.useFlowRun = useFlowRun;
|