@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.
@@ -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 };
@@ -1,5 +1,5 @@
1
- import { a as RunResult, R as RunOptions } from '../run-flow-AXzOR-Wq.cjs';
2
- export { r as runFlow } from '../run-flow-AXzOR-Wq.cjs';
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
 
@@ -1,5 +1,5 @@
1
- import { a as RunResult, R as RunOptions } from '../run-flow-9U-gaYVF.js';
2
- export { r as runFlow } from '../run-flow-9U-gaYVF.js';
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;