flowcraft 2.8.0 → 2.8.1
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 +1 -1
- package/dist/index-D3dyjW2G.d.mts +1269 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +727 -0
- package/dist/index.mjs.map +1 -0
- package/dist/replay-BB11M6K1.mjs +107 -0
- package/dist/replay-BB11M6K1.mjs.map +1 -0
- package/dist/runtime-CmefIAu_.mjs +2216 -0
- package/dist/runtime-CmefIAu_.mjs.map +1 -0
- package/dist/{sdk.d.ts → sdk.d.mts} +14 -12
- package/dist/sdk.mjs +29 -0
- package/dist/sdk.mjs.map +1 -0
- package/dist/testing/index.d.mts +172 -0
- package/dist/testing/index.mjs +277 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +13 -13
- package/dist/adapters/index.d.ts +0 -4
- package/dist/adapters/index.js +0 -4
- package/dist/adapters/index.js.map +0 -1
- package/dist/adapters/persistent-event-bus.d.ts +0 -69
- package/dist/adapters/persistent-event-bus.js +0 -3
- package/dist/adapters/persistent-event-bus.js.map +0 -1
- package/dist/analysis.d.ts +0 -53
- package/dist/analysis.js +0 -3
- package/dist/analysis.js.map +0 -1
- package/dist/chunk-26VTGQAF.js +0 -103
- package/dist/chunk-26VTGQAF.js.map +0 -1
- package/dist/chunk-27STBUGG.js +0 -44
- package/dist/chunk-27STBUGG.js.map +0 -1
- package/dist/chunk-2TSADFQX.js +0 -46
- package/dist/chunk-2TSADFQX.js.map +0 -1
- package/dist/chunk-3Y5O5EGB.js +0 -3
- package/dist/chunk-3Y5O5EGB.js.map +0 -1
- package/dist/chunk-4JUPOFSL.js +0 -76
- package/dist/chunk-4JUPOFSL.js.map +0 -1
- package/dist/chunk-4PELJWF7.js +0 -29
- package/dist/chunk-4PELJWF7.js.map +0 -1
- package/dist/chunk-55J6XMHW.js +0 -3
- package/dist/chunk-55J6XMHW.js.map +0 -1
- package/dist/chunk-5A24LVGQ.js +0 -90
- package/dist/chunk-5A24LVGQ.js.map +0 -1
- package/dist/chunk-6RKHCJUU.js +0 -29
- package/dist/chunk-6RKHCJUU.js.map +0 -1
- package/dist/chunk-7EBKWATZ.js +0 -86
- package/dist/chunk-7EBKWATZ.js.map +0 -1
- package/dist/chunk-BC4G7OM6.js +0 -42
- package/dist/chunk-BC4G7OM6.js.map +0 -1
- package/dist/chunk-BCRWXTWX.js +0 -21
- package/dist/chunk-BCRWXTWX.js.map +0 -1
- package/dist/chunk-BV6MXL74.js +0 -340
- package/dist/chunk-BV6MXL74.js.map +0 -1
- package/dist/chunk-CIXL7AKH.js +0 -532
- package/dist/chunk-CIXL7AKH.js.map +0 -1
- package/dist/chunk-DL7KVYZF.js +0 -39
- package/dist/chunk-DL7KVYZF.js.map +0 -1
- package/dist/chunk-E6ICIXVR.js +0 -25
- package/dist/chunk-E6ICIXVR.js.map +0 -1
- package/dist/chunk-HFJXYY4E.js +0 -3
- package/dist/chunk-HFJXYY4E.js.map +0 -1
- package/dist/chunk-HNHM3FDK.js +0 -52
- package/dist/chunk-HNHM3FDK.js.map +0 -1
- package/dist/chunk-I53JB2KW.js +0 -26
- package/dist/chunk-I53JB2KW.js.map +0 -1
- package/dist/chunk-IDTYHLDQ.js +0 -16
- package/dist/chunk-IDTYHLDQ.js.map +0 -1
- package/dist/chunk-IKOTX22J.js +0 -85
- package/dist/chunk-IKOTX22J.js.map +0 -1
- package/dist/chunk-K446NZ4E.js +0 -25
- package/dist/chunk-K446NZ4E.js.map +0 -1
- package/dist/chunk-L3MX5MTA.js +0 -33
- package/dist/chunk-L3MX5MTA.js.map +0 -1
- package/dist/chunk-L46TQXCV.js +0 -144
- package/dist/chunk-L46TQXCV.js.map +0 -1
- package/dist/chunk-LM4ACVHL.js +0 -73
- package/dist/chunk-LM4ACVHL.js.map +0 -1
- package/dist/chunk-LNK7LZER.js +0 -51
- package/dist/chunk-LNK7LZER.js.map +0 -1
- package/dist/chunk-NVLZFLYM.js +0 -3
- package/dist/chunk-NVLZFLYM.js.map +0 -1
- package/dist/chunk-ONH7PIJZ.js +0 -300
- package/dist/chunk-ONH7PIJZ.js.map +0 -1
- package/dist/chunk-OOJEXFYY.js +0 -47
- package/dist/chunk-OOJEXFYY.js.map +0 -1
- package/dist/chunk-PH2IYZHV.js +0 -48
- package/dist/chunk-PH2IYZHV.js.map +0 -1
- package/dist/chunk-PU2VTWJD.js +0 -164
- package/dist/chunk-PU2VTWJD.js.map +0 -1
- package/dist/chunk-QA2WDZPV.js +0 -74
- package/dist/chunk-QA2WDZPV.js.map +0 -1
- package/dist/chunk-RM677CNU.js +0 -52
- package/dist/chunk-RM677CNU.js.map +0 -1
- package/dist/chunk-TKSSRS5U.js +0 -39
- package/dist/chunk-TKSSRS5U.js.map +0 -1
- package/dist/chunk-UNORA7EM.js +0 -103
- package/dist/chunk-UNORA7EM.js.map +0 -1
- package/dist/chunk-VZ4BRDBK.js +0 -54
- package/dist/chunk-VZ4BRDBK.js.map +0 -1
- package/dist/chunk-X5RJOZY2.js +0 -279
- package/dist/chunk-X5RJOZY2.js.map +0 -1
- package/dist/chunk-ZETQCNEF.js +0 -139
- package/dist/chunk-ZETQCNEF.js.map +0 -1
- package/dist/chunk-ZKINHLMS.js +0 -230
- package/dist/chunk-ZKINHLMS.js.map +0 -1
- package/dist/chunk-ZLW4QOTS.js +0 -192
- package/dist/chunk-ZLW4QOTS.js.map +0 -1
- package/dist/container-factory.d.ts +0 -17
- package/dist/container-factory.js +0 -13
- package/dist/container-factory.js.map +0 -1
- package/dist/container.d.ts +0 -23
- package/dist/container.js +0 -3
- package/dist/container.js.map +0 -1
- package/dist/context.d.ts +0 -65
- package/dist/context.js +0 -3
- package/dist/context.js.map +0 -1
- package/dist/error-mapper.d.ts +0 -15
- package/dist/error-mapper.js +0 -4
- package/dist/error-mapper.js.map +0 -1
- package/dist/errors.d.ts +0 -20
- package/dist/errors.js +0 -3
- package/dist/errors.js.map +0 -1
- package/dist/evaluator.d.ts +0 -32
- package/dist/evaluator.js +0 -3
- package/dist/evaluator.js.map +0 -1
- package/dist/flow.d.ts +0 -83
- package/dist/flow.js +0 -4
- package/dist/flow.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/linter.d.ts +0 -26
- package/dist/linter.js +0 -4
- package/dist/linter.js.map +0 -1
- package/dist/logger.d.ts +0 -20
- package/dist/logger.js +0 -3
- package/dist/logger.js.map +0 -1
- package/dist/node.d.ts +0 -3
- package/dist/node.js +0 -3
- package/dist/node.js.map +0 -1
- package/dist/nodes/batch-gather.d.ts +0 -9
- package/dist/nodes/batch-gather.js +0 -4
- package/dist/nodes/batch-gather.js.map +0 -1
- package/dist/nodes/batch-scatter.d.ts +0 -9
- package/dist/nodes/batch-scatter.js +0 -4
- package/dist/nodes/batch-scatter.js.map +0 -1
- package/dist/nodes/sleep.d.ts +0 -9
- package/dist/nodes/sleep.js +0 -4
- package/dist/nodes/sleep.js.map +0 -1
- package/dist/nodes/subflow.d.ts +0 -9
- package/dist/nodes/subflow.js +0 -10
- package/dist/nodes/subflow.js.map +0 -1
- package/dist/nodes/wait.d.ts +0 -9
- package/dist/nodes/wait.js +0 -4
- package/dist/nodes/wait.js.map +0 -1
- package/dist/nodes/webhook.d.ts +0 -13
- package/dist/nodes/webhook.js +0 -4
- package/dist/nodes/webhook.js.map +0 -1
- package/dist/runtime/adapter.d.ts +0 -114
- package/dist/runtime/adapter.js +0 -28
- package/dist/runtime/adapter.js.map +0 -1
- package/dist/runtime/builtin-keys.d.ts +0 -38
- package/dist/runtime/builtin-keys.js +0 -10
- package/dist/runtime/builtin-keys.js.map +0 -1
- package/dist/runtime/execution-context.d.ts +0 -3
- package/dist/runtime/execution-context.js +0 -6
- package/dist/runtime/execution-context.js.map +0 -1
- package/dist/runtime/executors.d.ts +0 -3
- package/dist/runtime/executors.js +0 -4
- package/dist/runtime/executors.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -7
- package/dist/runtime/index.js +0 -31
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/node-executor-factory.d.ts +0 -12
- package/dist/runtime/node-executor-factory.js +0 -6
- package/dist/runtime/node-executor-factory.js.map +0 -1
- package/dist/runtime/orchestrator.d.ts +0 -9
- package/dist/runtime/orchestrator.js +0 -8
- package/dist/runtime/orchestrator.js.map +0 -1
- package/dist/runtime/orchestrators/replay.d.ts +0 -45
- package/dist/runtime/orchestrators/replay.js +0 -3
- package/dist/runtime/orchestrators/replay.js.map +0 -1
- package/dist/runtime/orchestrators/step-by-step.d.ts +0 -16
- package/dist/runtime/orchestrators/step-by-step.js +0 -5
- package/dist/runtime/orchestrators/step-by-step.js.map +0 -1
- package/dist/runtime/orchestrators/utils.d.ts +0 -35
- package/dist/runtime/orchestrators/utils.js +0 -4
- package/dist/runtime/orchestrators/utils.js.map +0 -1
- package/dist/runtime/runtime.d.ts +0 -3
- package/dist/runtime/runtime.js +0 -27
- package/dist/runtime/runtime.js.map +0 -1
- package/dist/runtime/scheduler.d.ts +0 -3
- package/dist/runtime/scheduler.js +0 -3
- package/dist/runtime/scheduler.js.map +0 -1
- package/dist/runtime/state.d.ts +0 -3
- package/dist/runtime/state.js +0 -5
- package/dist/runtime/state.js.map +0 -1
- package/dist/runtime/traverser.d.ts +0 -3
- package/dist/runtime/traverser.js +0 -5
- package/dist/runtime/traverser.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -3
- package/dist/runtime/types.js +0 -3
- package/dist/runtime/types.js.map +0 -1
- package/dist/runtime/workflow-logic-handler.d.ts +0 -17
- package/dist/runtime/workflow-logic-handler.js +0 -5
- package/dist/runtime/workflow-logic-handler.js.map +0 -1
- package/dist/sanitizer.d.ts +0 -12
- package/dist/sanitizer.js +0 -3
- package/dist/sanitizer.js.map +0 -1
- package/dist/sdk.js +0 -20
- package/dist/sdk.js.map +0 -1
- package/dist/serializer.d.ts +0 -18
- package/dist/serializer.js +0 -3
- package/dist/serializer.js.map +0 -1
- package/dist/testing/event-logger.d.ts +0 -63
- package/dist/testing/event-logger.js +0 -3
- package/dist/testing/event-logger.js.map +0 -1
- package/dist/testing/index.d.ts +0 -7
- package/dist/testing/index.js +0 -37
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/run-with-trace.d.ts +0 -38
- package/dist/testing/run-with-trace.js +0 -33
- package/dist/testing/run-with-trace.js.map +0 -1
- package/dist/testing/stepper.d.ts +0 -79
- package/dist/testing/stepper.js +0 -11
- package/dist/testing/stepper.js.map +0 -1
- package/dist/types-Biip2gLh.d.ts +0 -676
- package/dist/types.d.ts +0 -3
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Ct as WorkflowState, F as PersistentEventBusAdapter, K as NodeClass, N as IEventStore, P as InMemoryEventStore, R as FlowcraftEvent, V as IEventBus, X as NodeFunction, at as WorkflowBlueprint, ct as WorkflowResult, ht as GraphTraverser, ut as FlowRuntime } from "../index-D3dyjW2G.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/testing/event-logger.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A test utility that implements IEventBus to capture all workflow events
|
|
6
|
+
* in memory, acting as a "flight recorder" for behavioral testing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // In your test file (e.g., resiliency.test.ts)
|
|
10
|
+
* it('should retry a node on failure', async () => {
|
|
11
|
+
* const eventLogger = new InMemoryEventLogger();
|
|
12
|
+
* const runtime = new FlowRuntime({ eventBus: eventLogger });
|
|
13
|
+
*
|
|
14
|
+
* const flow = createFlow('retry-flow')
|
|
15
|
+
* .node('api-call', vi.fn().mockRejectedValueOnce(new Error('fail')), {
|
|
16
|
+
* config: { maxRetries: 2 },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* await runtime.run(flow.toBlueprint());
|
|
20
|
+
*
|
|
21
|
+
* // Assert against the captured event history to prove behavior.
|
|
22
|
+
* const retryEvents = eventLogger.filter('node:retry');
|
|
23
|
+
* expect(retryEvents).toHaveLength(1); // The first attempt is not a "retry"
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
declare class InMemoryEventLogger implements IEventBus {
|
|
27
|
+
readonly events: FlowcraftEvent[];
|
|
28
|
+
/**
|
|
29
|
+
* Clears all captured events.
|
|
30
|
+
*/
|
|
31
|
+
clear(): void;
|
|
32
|
+
/**
|
|
33
|
+
* The `emit` method required by the IEventBus interface.
|
|
34
|
+
* It simply pushes the received event into the internal events array.
|
|
35
|
+
* @param event The FlowcraftEvent to record.
|
|
36
|
+
*/
|
|
37
|
+
emit(event: FlowcraftEvent): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Finds the first event of a specific type.
|
|
40
|
+
* @param type The event type to find (e.g., 'node:error').
|
|
41
|
+
* @returns The first matching event, or undefined if not found.
|
|
42
|
+
*/
|
|
43
|
+
find<T extends FlowcraftEvent['type']>(type: T): Extract<FlowcraftEvent, {
|
|
44
|
+
type: T;
|
|
45
|
+
}> | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Filters events to find all occurrences of a specific type.
|
|
48
|
+
* @param type The event type to filter by.
|
|
49
|
+
* @returns An array of matching events.
|
|
50
|
+
*/
|
|
51
|
+
filter<T extends FlowcraftEvent['type']>(type: T): Extract<FlowcraftEvent, {
|
|
52
|
+
type: T;
|
|
53
|
+
}>[];
|
|
54
|
+
/**
|
|
55
|
+
* Prints a formatted log of all captured events to the console.
|
|
56
|
+
* Ideal for debugging failing tests.
|
|
57
|
+
* @param title A title for the log output.
|
|
58
|
+
*/
|
|
59
|
+
printLog(title?: string): void;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/testing/run-with-trace.d.ts
|
|
63
|
+
/**
|
|
64
|
+
* A test helper that executes a workflow and automatically prints a detailed
|
|
65
|
+
* execution trace to the console if the workflow fails.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // In your test file (e.g., my-workflow.test.ts)
|
|
69
|
+
* it('should process data correctly', async () => {
|
|
70
|
+
* const flow = createFlow('my-flow')
|
|
71
|
+
* .node('a', async () => ({ output: 1 }))
|
|
72
|
+
* .node('b', async ({ input }) => ({ output: input + 1 })) // Bug: returns { output: 3 }
|
|
73
|
+
* .edge('a', 'b')
|
|
74
|
+
*
|
|
75
|
+
* const runtime = new FlowRuntime({})
|
|
76
|
+
*
|
|
77
|
+
* // If this test fails, a full, human-readable trace of the execution
|
|
78
|
+
* // (inputs, outputs, context changes) is printed to the console.
|
|
79
|
+
* const result = await runWithTrace(runtime, flow.toBlueprint())
|
|
80
|
+
*
|
|
81
|
+
* expect(result.context.b).toBe(2)
|
|
82
|
+
* })
|
|
83
|
+
*
|
|
84
|
+
* @param runtime The original FlowRuntime instance (its options will be used).
|
|
85
|
+
* @param blueprint The WorkflowBlueprint to execute.
|
|
86
|
+
* @param initialState The initial state for the workflow run.
|
|
87
|
+
* @param options Additional options for the run.
|
|
88
|
+
* @returns The WorkflowResult if successful.
|
|
89
|
+
*/
|
|
90
|
+
declare function runWithTrace<TContext extends Record<string, any>>(runtime: FlowRuntime<TContext, any>, blueprint: WorkflowBlueprint, initialState?: Partial<TContext> | string, options?: {
|
|
91
|
+
functionRegistry?: Map<string, any>;
|
|
92
|
+
strict?: boolean;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}): Promise<WorkflowResult<Record<string, any>>>;
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/testing/stepper.d.ts
|
|
97
|
+
/**
|
|
98
|
+
* Represents the controlled, step-by-step execution of a workflow.
|
|
99
|
+
* Returned by the `createStepper` utility.
|
|
100
|
+
*/
|
|
101
|
+
interface IWorkflowStepper<TContext extends Record<string, any>> {
|
|
102
|
+
/** The current state of the workflow. Can be inspected after each step. */
|
|
103
|
+
readonly state: WorkflowState<TContext>;
|
|
104
|
+
/** The graph traverser instance. Can be used to inspect the frontier or completed nodes. */
|
|
105
|
+
readonly traverser: GraphTraverser;
|
|
106
|
+
/**
|
|
107
|
+
* Executes the next "turn" or batch of ready nodes in the workflow.
|
|
108
|
+
* @param options Optional configuration for this specific step, like a cancellation signal.
|
|
109
|
+
* @returns A `WorkflowResult` representing the state after the step, or `null` if the workflow has already completed.
|
|
110
|
+
*/
|
|
111
|
+
next(options?: {
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
concurrency?: number;
|
|
114
|
+
}): Promise<WorkflowResult<TContext> | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Reverts the workflow to its previous state.
|
|
117
|
+
* @returns The `WorkflowResult` of the previous state, or `null` if there is no history to revert to.
|
|
118
|
+
*/
|
|
119
|
+
prev(): Promise<WorkflowResult<TContext> | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Resets the stepper to its initial state, clearing all progress and history.
|
|
122
|
+
*/
|
|
123
|
+
reset(): void;
|
|
124
|
+
/**
|
|
125
|
+
* A convenience method to check if the workflow has any more steps to run.
|
|
126
|
+
* @returns `true` if the workflow is complete or stalled, `false` otherwise.
|
|
127
|
+
*/
|
|
128
|
+
isDone(): boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A test utility that creates a stepper to execute a workflow one "turn" at a time.
|
|
132
|
+
* This is invaluable for debugging and writing fine-grained tests where you need to
|
|
133
|
+
* assert the state of the workflow after each logical step.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* // In your test file
|
|
137
|
+
* it('should correctly execute step-by-step', async () => {
|
|
138
|
+
* const runtime = new FlowRuntime({ ... });
|
|
139
|
+
* const flow = createFlow('test')
|
|
140
|
+
* .node('a', async () => ({ output: 10 }))
|
|
141
|
+
* .node('b', async ({ input }) => ({ output: input * 2 }))
|
|
142
|
+
* .edge('a', 'b');
|
|
143
|
+
*
|
|
144
|
+
* const stepper = await createStepper(runtime, flow.toBlueprint(), flow.getFunctionRegistry());
|
|
145
|
+
*
|
|
146
|
+
* // First step (executes node 'a')
|
|
147
|
+
* const result1 = await stepper.next();
|
|
148
|
+
* expect(stepper.isDone()).toBe(false);
|
|
149
|
+
* expect(result1.status).toBe('stalled');
|
|
150
|
+
* expect(result1.context._outputs.a).toBe(10);
|
|
151
|
+
*
|
|
152
|
+
* // Second step (executes node 'b')
|
|
153
|
+
* const result2 = await stepper.next();
|
|
154
|
+
* expect(stepper.isDone()).toBe(true);
|
|
155
|
+
* expect(result2.status).toBe('completed');
|
|
156
|
+
* expect(result2.context._outputs.b).toBe(20);
|
|
157
|
+
*
|
|
158
|
+
* // Final step (no more work)
|
|
159
|
+
* const result3 = await stepper.next();
|
|
160
|
+
* expect(result3).toBeNull();
|
|
161
|
+
* });
|
|
162
|
+
*
|
|
163
|
+
* @param runtime The `FlowRuntime` instance, used for its configuration.
|
|
164
|
+
* @param blueprint The `WorkflowBlueprint` to execute.
|
|
165
|
+
* @param functionRegistry The function registry from createFlow, containing the node implementations.
|
|
166
|
+
* @param initialState The initial state for the workflow run.
|
|
167
|
+
* @returns A Promise that resolves to an `IWorkflowStepper` instance.
|
|
168
|
+
*/
|
|
169
|
+
declare function createStepper<TContext extends Record<string, any>, TDependencies extends Record<string, any>>(runtime: FlowRuntime<TContext, TDependencies>, blueprint: WorkflowBlueprint, functionRegistry: Map<string, NodeFunction | NodeClass>, initialState?: Partial<TContext>): Promise<IWorkflowStepper<TContext>>;
|
|
170
|
+
//#endregion
|
|
171
|
+
export { IEventStore, IWorkflowStepper, InMemoryEventLogger, InMemoryEventStore, PersistentEventBusAdapter, createStepper, runWithTrace };
|
|
172
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { A as PersistentEventBusAdapter, d as executeBatch, f as processResults, k as InMemoryEventStore, m as WorkflowState, o as GraphTraverser, p as ExecutionContext, t as FlowRuntime, v as FlowcraftError } from "../runtime-CmefIAu_.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/testing/event-logger.ts
|
|
4
|
+
/**
|
|
5
|
+
* A test utility that implements IEventBus to capture all workflow events
|
|
6
|
+
* in memory, acting as a "flight recorder" for behavioral testing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // In your test file (e.g., resiliency.test.ts)
|
|
10
|
+
* it('should retry a node on failure', async () => {
|
|
11
|
+
* const eventLogger = new InMemoryEventLogger();
|
|
12
|
+
* const runtime = new FlowRuntime({ eventBus: eventLogger });
|
|
13
|
+
*
|
|
14
|
+
* const flow = createFlow('retry-flow')
|
|
15
|
+
* .node('api-call', vi.fn().mockRejectedValueOnce(new Error('fail')), {
|
|
16
|
+
* config: { maxRetries: 2 },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* await runtime.run(flow.toBlueprint());
|
|
20
|
+
*
|
|
21
|
+
* // Assert against the captured event history to prove behavior.
|
|
22
|
+
* const retryEvents = eventLogger.filter('node:retry');
|
|
23
|
+
* expect(retryEvents).toHaveLength(1); // The first attempt is not a "retry"
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
var InMemoryEventLogger = class {
|
|
27
|
+
events = [];
|
|
28
|
+
/**
|
|
29
|
+
* Clears all captured events.
|
|
30
|
+
*/
|
|
31
|
+
clear() {
|
|
32
|
+
this.events.length = 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The `emit` method required by the IEventBus interface.
|
|
36
|
+
* It simply pushes the received event into the internal events array.
|
|
37
|
+
* @param event The FlowcraftEvent to record.
|
|
38
|
+
*/
|
|
39
|
+
async emit(event) {
|
|
40
|
+
this.events.push(event);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Finds the first event of a specific type.
|
|
44
|
+
* @param type The event type to find (e.g., 'node:error').
|
|
45
|
+
* @returns The first matching event, or undefined if not found.
|
|
46
|
+
*/
|
|
47
|
+
find(type) {
|
|
48
|
+
return this.events.find((e) => e.type === type);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Filters events to find all occurrences of a specific type.
|
|
52
|
+
* @param type The event type to filter by.
|
|
53
|
+
* @returns An array of matching events.
|
|
54
|
+
*/
|
|
55
|
+
filter(type) {
|
|
56
|
+
return this.events.filter((e) => e.type === type);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Prints a formatted log of all captured events to the console.
|
|
60
|
+
* Ideal for debugging failing tests.
|
|
61
|
+
* @param title A title for the log output.
|
|
62
|
+
*/
|
|
63
|
+
printLog(title = "Workflow Execution Trace") {
|
|
64
|
+
console.log(`\n--- ${title} ---`);
|
|
65
|
+
if (this.events.length === 0) {
|
|
66
|
+
console.log("No events were captured.");
|
|
67
|
+
console.log("----------------------------------\n");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.events.forEach((event, index) => {
|
|
71
|
+
const { type, payload } = event;
|
|
72
|
+
console.log(`\n[${index + 1}] ${type}`);
|
|
73
|
+
switch (type) {
|
|
74
|
+
case "node:start":
|
|
75
|
+
console.log(` - Node: "${payload.nodeId}" | Input: ${JSON.stringify(payload.input)}`);
|
|
76
|
+
break;
|
|
77
|
+
case "edge:evaluate":
|
|
78
|
+
console.log(` - Edge: "${payload.source}" -> "${payload.target}"`);
|
|
79
|
+
console.log(` - Condition: ${payload.condition || "N/A"} | Result: ${payload.result}`);
|
|
80
|
+
break;
|
|
81
|
+
case "context:change":
|
|
82
|
+
if (payload.op === "set") console.log(` - Node "${payload.sourceNode}" wrote to context -> Key: "${payload.key}" | Value: ${JSON.stringify(payload.value)}`);
|
|
83
|
+
else if (payload.op === "delete") console.log(` - Node "${payload.sourceNode}" deleted from context -> Key: "${payload.key}"`);
|
|
84
|
+
break;
|
|
85
|
+
case "node:finish":
|
|
86
|
+
console.log(` - Node: "${payload.nodeId}" | Result: ${JSON.stringify(payload.result)}`);
|
|
87
|
+
break;
|
|
88
|
+
case "node:error":
|
|
89
|
+
console.log(` - Node: "${payload.nodeId}"`);
|
|
90
|
+
console.error(" - Error:", payload.error);
|
|
91
|
+
break;
|
|
92
|
+
default: console.log(` - Payload: ${JSON.stringify(payload, null, 2)}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
console.log(`\n--- End of Trace ---`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/testing/run-with-trace.ts
|
|
101
|
+
/**
|
|
102
|
+
* A test helper that executes a workflow and automatically prints a detailed
|
|
103
|
+
* execution trace to the console if the workflow fails.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // In your test file (e.g., my-workflow.test.ts)
|
|
107
|
+
* it('should process data correctly', async () => {
|
|
108
|
+
* const flow = createFlow('my-flow')
|
|
109
|
+
* .node('a', async () => ({ output: 1 }))
|
|
110
|
+
* .node('b', async ({ input }) => ({ output: input + 1 })) // Bug: returns { output: 3 }
|
|
111
|
+
* .edge('a', 'b')
|
|
112
|
+
*
|
|
113
|
+
* const runtime = new FlowRuntime({})
|
|
114
|
+
*
|
|
115
|
+
* // If this test fails, a full, human-readable trace of the execution
|
|
116
|
+
* // (inputs, outputs, context changes) is printed to the console.
|
|
117
|
+
* const result = await runWithTrace(runtime, flow.toBlueprint())
|
|
118
|
+
*
|
|
119
|
+
* expect(result.context.b).toBe(2)
|
|
120
|
+
* })
|
|
121
|
+
*
|
|
122
|
+
* @param runtime The original FlowRuntime instance (its options will be used).
|
|
123
|
+
* @param blueprint The WorkflowBlueprint to execute.
|
|
124
|
+
* @param initialState The initial state for the workflow run.
|
|
125
|
+
* @param options Additional options for the run.
|
|
126
|
+
* @returns The WorkflowResult if successful.
|
|
127
|
+
*/
|
|
128
|
+
async function runWithTrace(runtime, blueprint, initialState = {}, options = {}) {
|
|
129
|
+
const eventLogger = new InMemoryEventLogger();
|
|
130
|
+
const testRuntime = new FlowRuntime({
|
|
131
|
+
...runtime.options,
|
|
132
|
+
eventBus: eventLogger
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const result = await testRuntime.run(blueprint, initialState, options);
|
|
136
|
+
if (process.env.DEBUG) eventLogger.printLog(`Successful Trace: ${blueprint.id}`);
|
|
137
|
+
return result;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
eventLogger.printLog(`Failing Test Trace: ${blueprint.id}`);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/runtime/orchestrators/step-by-step.ts
|
|
146
|
+
/**
|
|
147
|
+
* An orchestrator that executes only one "tick" or "turn" of the workflow.
|
|
148
|
+
* It processes a single batch of ready nodes from the frontier and then returns,
|
|
149
|
+
* allowing the caller to inspect the intermediate state before proceeding.
|
|
150
|
+
*
|
|
151
|
+
* Useful for debugging, testing, or building interactive tools.
|
|
152
|
+
*/
|
|
153
|
+
var StepByStepOrchestrator = class {
|
|
154
|
+
async run(context, traverser) {
|
|
155
|
+
try {
|
|
156
|
+
context.signal?.throwIfAborted();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", { isFatal: false });
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (!traverser.hasMoreWork()) {
|
|
162
|
+
const isTraversalComplete = !traverser.hasMoreWork();
|
|
163
|
+
const status = context.state.getStatus(isTraversalComplete);
|
|
164
|
+
const result = await context.state.toResult(context.services.serializer, context.executionId);
|
|
165
|
+
result.status = status;
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
const allReadyNodes = traverser.getReadyNodes();
|
|
169
|
+
const nodesToExecute = context.concurrency ? allReadyNodes.slice(0, context.concurrency) : allReadyNodes;
|
|
170
|
+
const nodesToSkip = context.concurrency ? allReadyNodes.slice(context.concurrency) : [];
|
|
171
|
+
await processResults(await executeBatch(nodesToExecute, traverser.getDynamicBlueprint(), context.state, (nodeId) => context.runtime.getExecutorForNode(nodeId, context), context.runtime, context.concurrency), traverser, context.state, context.runtime, context.blueprint, context.executionId);
|
|
172
|
+
for (const { nodeId } of nodesToSkip) traverser.addToFrontier(nodeId);
|
|
173
|
+
const isTraversalComplete = !traverser.hasMoreWork();
|
|
174
|
+
const status = context.state.getStatus(isTraversalComplete);
|
|
175
|
+
const result = await context.state.toResult(context.services.serializer, context.executionId);
|
|
176
|
+
result.status = status;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/testing/stepper.ts
|
|
183
|
+
/**
|
|
184
|
+
* A test utility that creates a stepper to execute a workflow one "turn" at a time.
|
|
185
|
+
* This is invaluable for debugging and writing fine-grained tests where you need to
|
|
186
|
+
* assert the state of the workflow after each logical step.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* // In your test file
|
|
190
|
+
* it('should correctly execute step-by-step', async () => {
|
|
191
|
+
* const runtime = new FlowRuntime({ ... });
|
|
192
|
+
* const flow = createFlow('test')
|
|
193
|
+
* .node('a', async () => ({ output: 10 }))
|
|
194
|
+
* .node('b', async ({ input }) => ({ output: input * 2 }))
|
|
195
|
+
* .edge('a', 'b');
|
|
196
|
+
*
|
|
197
|
+
* const stepper = await createStepper(runtime, flow.toBlueprint(), flow.getFunctionRegistry());
|
|
198
|
+
*
|
|
199
|
+
* // First step (executes node 'a')
|
|
200
|
+
* const result1 = await stepper.next();
|
|
201
|
+
* expect(stepper.isDone()).toBe(false);
|
|
202
|
+
* expect(result1.status).toBe('stalled');
|
|
203
|
+
* expect(result1.context._outputs.a).toBe(10);
|
|
204
|
+
*
|
|
205
|
+
* // Second step (executes node 'b')
|
|
206
|
+
* const result2 = await stepper.next();
|
|
207
|
+
* expect(stepper.isDone()).toBe(true);
|
|
208
|
+
* expect(result2.status).toBe('completed');
|
|
209
|
+
* expect(result2.context._outputs.b).toBe(20);
|
|
210
|
+
*
|
|
211
|
+
* // Final step (no more work)
|
|
212
|
+
* const result3 = await stepper.next();
|
|
213
|
+
* expect(result3).toBeNull();
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* @param runtime The `FlowRuntime` instance, used for its configuration.
|
|
217
|
+
* @param blueprint The `WorkflowBlueprint` to execute.
|
|
218
|
+
* @param functionRegistry The function registry from createFlow, containing the node implementations.
|
|
219
|
+
* @param initialState The initial state for the workflow run.
|
|
220
|
+
* @returns A Promise that resolves to an `IWorkflowStepper` instance.
|
|
221
|
+
*/
|
|
222
|
+
async function createStepper(runtime, blueprint, functionRegistry, initialState = {}) {
|
|
223
|
+
const _initialBlueprint = structuredClone(blueprint);
|
|
224
|
+
const _initialState = structuredClone(initialState);
|
|
225
|
+
let state;
|
|
226
|
+
let traverser;
|
|
227
|
+
const history = [];
|
|
228
|
+
const orchestrator = new StepByStepOrchestrator();
|
|
229
|
+
const executionId = globalThis.crypto?.randomUUID();
|
|
230
|
+
const nodeRegistry = new Map([...runtime.registry, ...functionRegistry]);
|
|
231
|
+
const initialize = () => {
|
|
232
|
+
state = new WorkflowState(_initialState);
|
|
233
|
+
traverser = new GraphTraverser(_initialBlueprint);
|
|
234
|
+
history.length = 0;
|
|
235
|
+
};
|
|
236
|
+
initialize();
|
|
237
|
+
const stepper = {
|
|
238
|
+
get state() {
|
|
239
|
+
return state;
|
|
240
|
+
},
|
|
241
|
+
get traverser() {
|
|
242
|
+
return traverser;
|
|
243
|
+
},
|
|
244
|
+
isDone() {
|
|
245
|
+
return !traverser.hasMoreWork() && !state.isAwaiting();
|
|
246
|
+
},
|
|
247
|
+
reset() {
|
|
248
|
+
initialize();
|
|
249
|
+
},
|
|
250
|
+
async prev() {
|
|
251
|
+
const previousStateJson = history.pop();
|
|
252
|
+
if (!previousStateJson) return null;
|
|
253
|
+
state = new WorkflowState(runtime.serializer.deserialize(previousStateJson));
|
|
254
|
+
traverser = GraphTraverser.fromState(_initialBlueprint, state);
|
|
255
|
+
return state.toResult(runtime.serializer, void 0);
|
|
256
|
+
},
|
|
257
|
+
async next(options = {}) {
|
|
258
|
+
if (stepper.isDone()) return null;
|
|
259
|
+
const serializedContext = (await state.toResult(runtime.serializer, void 0)).serializedContext;
|
|
260
|
+
history.push(serializedContext);
|
|
261
|
+
const executionContext = new ExecutionContext(_initialBlueprint, state, nodeRegistry, executionId, runtime, {
|
|
262
|
+
logger: runtime.logger,
|
|
263
|
+
eventBus: runtime.eventBus,
|
|
264
|
+
serializer: runtime.serializer,
|
|
265
|
+
evaluator: runtime.evaluator,
|
|
266
|
+
middleware: runtime.middleware,
|
|
267
|
+
dependencies: runtime.dependencies
|
|
268
|
+
}, options.signal, options.concurrency);
|
|
269
|
+
return orchestrator.run(executionContext, traverser);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
return stepper;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
export { InMemoryEventLogger, InMemoryEventStore, PersistentEventBusAdapter, createStepper, runWithTrace };
|
|
277
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/testing/event-logger.ts","../../src/testing/run-with-trace.ts","../../src/runtime/orchestrators/step-by-step.ts","../../src/testing/stepper.ts"],"sourcesContent":["import type { FlowcraftEvent, IEventBus } from '../types'\n\n/**\n * A test utility that implements IEventBus to capture all workflow events\n * in memory, acting as a \"flight recorder\" for behavioral testing.\n *\n * @example\n * // In your test file (e.g., resiliency.test.ts)\n * it('should retry a node on failure', async () => {\n * const eventLogger = new InMemoryEventLogger();\n * const runtime = new FlowRuntime({ eventBus: eventLogger });\n *\n * const flow = createFlow('retry-flow')\n * .node('api-call', vi.fn().mockRejectedValueOnce(new Error('fail')), {\n * config: { maxRetries: 2 },\n * });\n *\n * await runtime.run(flow.toBlueprint());\n *\n * // Assert against the captured event history to prove behavior.\n * const retryEvents = eventLogger.filter('node:retry');\n * expect(retryEvents).toHaveLength(1); // The first attempt is not a \"retry\"\n * });\n */\nexport class InMemoryEventLogger implements IEventBus {\n\tpublic readonly events: FlowcraftEvent[] = []\n\n\t/**\n\t * Clears all captured events.\n\t */\n\tpublic clear(): void {\n\t\tthis.events.length = 0\n\t}\n\n\t/**\n\t * The `emit` method required by the IEventBus interface.\n\t * It simply pushes the received event into the internal events array.\n\t * @param event The FlowcraftEvent to record.\n\t */\n\tpublic async emit(event: FlowcraftEvent): Promise<void> {\n\t\tthis.events.push(event)\n\t}\n\n\t/**\n\t * Finds the first event of a specific type.\n\t * @param type The event type to find (e.g., 'node:error').\n\t * @returns The first matching event, or undefined if not found.\n\t */\n\tpublic find<T extends FlowcraftEvent['type']>(type: T): Extract<FlowcraftEvent, { type: T }> | undefined {\n\t\treturn this.events.find((e) => e.type === type) as Extract<FlowcraftEvent, { type: T }> | undefined\n\t}\n\n\t/**\n\t * Filters events to find all occurrences of a specific type.\n\t * @param type The event type to filter by.\n\t * @returns An array of matching events.\n\t */\n\tpublic filter<T extends FlowcraftEvent['type']>(type: T): Extract<FlowcraftEvent, { type: T }>[] {\n\t\treturn this.events.filter((e) => e.type === type) as Extract<FlowcraftEvent, { type: T }>[]\n\t}\n\n\t/**\n\t * Prints a formatted log of all captured events to the console.\n\t * Ideal for debugging failing tests.\n\t * @param title A title for the log output.\n\t */\n\tpublic printLog(title = 'Workflow Execution Trace'): void {\n\t\tconsole.log(`\\n--- ${title} ---`)\n\t\tif (this.events.length === 0) {\n\t\t\tconsole.log('No events were captured.')\n\t\t\tconsole.log('----------------------------------\\n')\n\t\t\treturn\n\t\t}\n\n\t\tthis.events.forEach((event, index) => {\n\t\t\tconst { type, payload } = event\n\t\t\tconsole.log(`\\n[${index + 1}] ${type}`)\n\n\t\t\t// Custom formatting for a more intuitive trace\n\t\t\tswitch (type) {\n\t\t\t\tcase 'node:start':\n\t\t\t\t\tconsole.log(` - Node: \"${payload.nodeId}\" | Input: ${JSON.stringify(payload.input)}`)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'edge:evaluate':\n\t\t\t\t\tconsole.log(` - Edge: \"${payload.source}\" -> \"${payload.target}\"`)\n\t\t\t\t\tconsole.log(` - Condition: ${payload.condition || 'N/A'} | Result: ${payload.result}`)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'context:change':\n\t\t\t\t\tif (payload.op === 'set') {\n\t\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t\t` - Node \"${payload.sourceNode}\" wrote to context -> Key: \"${payload.key}\" | Value: ${JSON.stringify(payload.value)}`,\n\t\t\t\t\t\t)\n\t\t\t\t\t} else if (payload.op === 'delete') {\n\t\t\t\t\t\tconsole.log(` - Node \"${payload.sourceNode}\" deleted from context -> Key: \"${payload.key}\"`)\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\tcase 'node:finish':\n\t\t\t\t\tconsole.log(` - Node: \"${payload.nodeId}\" | Result: ${JSON.stringify(payload.result)}`)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'node:error':\n\t\t\t\t\tconsole.log(` - Node: \"${payload.nodeId}\"`)\n\t\t\t\t\tconsole.error(' - Error:', payload.error)\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\tconsole.log(` - Payload: ${JSON.stringify(payload, null, 2)}`)\n\t\t\t}\n\t\t})\n\t\tconsole.log(`\\n--- End of Trace ---`)\n\t}\n}\n","import { FlowRuntime } from '../runtime'\nimport type { WorkflowBlueprint } from '../types'\nimport { InMemoryEventLogger } from './event-logger'\n\n/**\n * A test helper that executes a workflow and automatically prints a detailed\n * execution trace to the console if the workflow fails.\n *\n * @example\n * // In your test file (e.g., my-workflow.test.ts)\n * it('should process data correctly', async () => {\n * const flow = createFlow('my-flow')\n * .node('a', async () => ({ output: 1 }))\n * .node('b', async ({ input }) => ({ output: input + 1 })) // Bug: returns { output: 3 }\n * .edge('a', 'b')\n *\n * const runtime = new FlowRuntime({})\n *\n * // If this test fails, a full, human-readable trace of the execution\n * // (inputs, outputs, context changes) is printed to the console.\n * const result = await runWithTrace(runtime, flow.toBlueprint())\n *\n * expect(result.context.b).toBe(2)\n * })\n *\n * @param runtime The original FlowRuntime instance (its options will be used).\n * @param blueprint The WorkflowBlueprint to execute.\n * @param initialState The initial state for the workflow run.\n * @param options Additional options for the run.\n * @returns The WorkflowResult if successful.\n */\nexport async function runWithTrace<TContext extends Record<string, any>>(\n\truntime: FlowRuntime<TContext, any>,\n\tblueprint: WorkflowBlueprint,\n\tinitialState: Partial<TContext> | string = {},\n\toptions: {\n\t\tfunctionRegistry?: Map<string, any>\n\t\tstrict?: boolean\n\t\tsignal?: AbortSignal\n\t} = {},\n) {\n\tconst eventLogger = new InMemoryEventLogger()\n\tconst testRuntime = new FlowRuntime({\n\t\t...runtime.options,\n\t\teventBus: eventLogger,\n\t})\n\n\ttry {\n\t\tconst result = await testRuntime.run(blueprint, initialState, options)\n\t\tif (process.env.DEBUG) {\n\t\t\teventLogger.printLog(`Successful Trace: ${blueprint.id}`)\n\t\t}\n\t\treturn result\n\t} catch (error) {\n\t\teventLogger.printLog(`Failing Test Trace: ${blueprint.id}`)\n\t\tthrow error\n\t}\n}\n","import { FlowcraftError } from '../../errors'\n\nimport type { WorkflowResult } from '../../types'\nimport type { ExecutionContext } from '../execution-context'\nimport type { GraphTraverser } from '../traverser'\nimport type { IOrchestrator } from '../types'\nimport { executeBatch, processResults } from './utils'\n\n/**\n * An orchestrator that executes only one \"tick\" or \"turn\" of the workflow.\n * It processes a single batch of ready nodes from the frontier and then returns,\n * allowing the caller to inspect the intermediate state before proceeding.\n *\n * Useful for debugging, testing, or building interactive tools.\n */\nexport class StepByStepOrchestrator implements IOrchestrator {\n\tpublic async run(context: ExecutionContext<any, any>, traverser: GraphTraverser): Promise<WorkflowResult<any>> {\n\t\ttry {\n\t\t\tcontext.signal?.throwIfAborted()\n\t\t} catch (error) {\n\t\t\tif (error instanceof DOMException && error.name === 'AbortError') {\n\t\t\t\tthrow new FlowcraftError('Workflow cancelled', { isFatal: false })\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\n\t\tif (!traverser.hasMoreWork()) {\n\t\t\tconst isTraversalComplete = !traverser.hasMoreWork()\n\t\t\tconst status = context.state.getStatus(isTraversalComplete)\n\t\t\tconst result = await context.state.toResult(context.services.serializer, context.executionId)\n\t\t\tresult.status = status\n\t\t\treturn result\n\t\t}\n\n\t\tconst allReadyNodes = traverser.getReadyNodes()\n\t\tconst nodesToExecute = context.concurrency ? allReadyNodes.slice(0, context.concurrency) : allReadyNodes\n\t\tconst nodesToSkip = context.concurrency ? allReadyNodes.slice(context.concurrency) : []\n\n\t\tconst settledResults = await executeBatch(\n\t\t\tnodesToExecute,\n\t\t\ttraverser.getDynamicBlueprint(),\n\t\t\tcontext.state,\n\t\t\t(nodeId: string) => context.runtime.getExecutorForNode(nodeId, context),\n\t\t\tcontext.runtime,\n\t\t\tcontext.concurrency,\n\t\t)\n\n\t\tawait processResults(\n\t\t\tsettledResults,\n\t\t\ttraverser,\n\t\t\tcontext.state,\n\t\t\tcontext.runtime,\n\t\t\tcontext.blueprint,\n\t\t\tcontext.executionId,\n\t\t)\n\n\t\tfor (const { nodeId } of nodesToSkip) {\n\t\t\ttraverser.addToFrontier(nodeId)\n\t\t}\n\n\t\tconst isTraversalComplete = !traverser.hasMoreWork()\n\t\tconst status = context.state.getStatus(isTraversalComplete)\n\t\tconst result = await context.state.toResult(context.services.serializer, context.executionId)\n\t\tresult.status = status\n\t\treturn result\n\t}\n}\n","import type { FlowRuntime } from '../runtime'\nimport { ExecutionContext } from '../runtime/execution-context'\nimport { StepByStepOrchestrator } from '../runtime/orchestrators/step-by-step'\nimport { WorkflowState } from '../runtime/state'\nimport { GraphTraverser } from '../runtime/traverser'\nimport type { NodeClass, NodeFunction, WorkflowBlueprint, WorkflowResult } from '../types'\n\n/**\n * Represents the controlled, step-by-step execution of a workflow.\n * Returned by the `createStepper` utility.\n */\nexport interface IWorkflowStepper<TContext extends Record<string, any>> {\n\t/** The current state of the workflow. Can be inspected after each step. */\n\treadonly state: WorkflowState<TContext>\n\n\t/** The graph traverser instance. Can be used to inspect the frontier or completed nodes. */\n\treadonly traverser: GraphTraverser\n\n\t/**\n\t * Executes the next \"turn\" or batch of ready nodes in the workflow.\n\t * @param options Optional configuration for this specific step, like a cancellation signal.\n\t * @returns A `WorkflowResult` representing the state after the step, or `null` if the workflow has already completed.\n\t */\n\tnext(options?: { signal?: AbortSignal; concurrency?: number }): Promise<WorkflowResult<TContext> | null>\n\n\t/**\n\t * Reverts the workflow to its previous state.\n\t * @returns The `WorkflowResult` of the previous state, or `null` if there is no history to revert to.\n\t */\n\tprev(): Promise<WorkflowResult<TContext> | null>\n\n\t/**\n\t * Resets the stepper to its initial state, clearing all progress and history.\n\t */\n\treset(): void\n\n\t/**\n\t * A convenience method to check if the workflow has any more steps to run.\n\t * @returns `true` if the workflow is complete or stalled, `false` otherwise.\n\t */\n\tisDone(): boolean\n}\n\n/**\n * A test utility that creates a stepper to execute a workflow one \"turn\" at a time.\n * This is invaluable for debugging and writing fine-grained tests where you need to\n * assert the state of the workflow after each logical step.\n *\n * @example\n * // In your test file\n * it('should correctly execute step-by-step', async () => {\n * const runtime = new FlowRuntime({ ... });\n * const flow = createFlow('test')\n * .node('a', async () => ({ output: 10 }))\n * .node('b', async ({ input }) => ({ output: input * 2 }))\n * .edge('a', 'b');\n *\n * const stepper = await createStepper(runtime, flow.toBlueprint(), flow.getFunctionRegistry());\n *\n * // First step (executes node 'a')\n * const result1 = await stepper.next();\n * expect(stepper.isDone()).toBe(false);\n * expect(result1.status).toBe('stalled');\n * expect(result1.context._outputs.a).toBe(10);\n *\n * // Second step (executes node 'b')\n * const result2 = await stepper.next();\n * expect(stepper.isDone()).toBe(true);\n * expect(result2.status).toBe('completed');\n * expect(result2.context._outputs.b).toBe(20);\n *\n * // Final step (no more work)\n * const result3 = await stepper.next();\n * expect(result3).toBeNull();\n * });\n *\n * @param runtime The `FlowRuntime` instance, used for its configuration.\n * @param blueprint The `WorkflowBlueprint` to execute.\n * @param functionRegistry The function registry from createFlow, containing the node implementations.\n * @param initialState The initial state for the workflow run.\n * @returns A Promise that resolves to an `IWorkflowStepper` instance.\n */\nexport async function createStepper<TContext extends Record<string, any>, TDependencies extends Record<string, any>>(\n\truntime: FlowRuntime<TContext, TDependencies>,\n\tblueprint: WorkflowBlueprint,\n\tfunctionRegistry: Map<string, NodeFunction | NodeClass>,\n\tinitialState: Partial<TContext> = {},\n): Promise<IWorkflowStepper<TContext>> {\n\tconst _initialBlueprint = structuredClone(blueprint)\n\tconst _initialState = structuredClone(initialState)\n\n\tlet state: WorkflowState<TContext>\n\tlet traverser: GraphTraverser\n\tconst history: string[] = []\n\n\tconst orchestrator = new StepByStepOrchestrator()\n\tconst executionId = globalThis.crypto?.randomUUID()\n\tconst nodeRegistry = new Map([...runtime.registry, ...functionRegistry])\n\n\tconst initialize = () => {\n\t\tstate = new WorkflowState<TContext>(_initialState)\n\t\ttraverser = new GraphTraverser(_initialBlueprint)\n\t\thistory.length = 0\n\t}\n\n\tinitialize()\n\n\tconst stepper: IWorkflowStepper<TContext> = {\n\t\tget state() {\n\t\t\treturn state\n\t\t},\n\t\tget traverser() {\n\t\t\treturn traverser\n\t\t},\n\t\tisDone() {\n\t\t\treturn !traverser.hasMoreWork() && !state.isAwaiting()\n\t\t},\n\t\treset() {\n\t\t\tinitialize()\n\t\t},\n\t\tasync prev() {\n\t\t\tconst previousStateJson = history.pop()\n\t\t\tif (!previousStateJson) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst previousStateData = runtime.serializer.deserialize(previousStateJson) as Partial<TContext>\n\n\t\t\tstate = new WorkflowState(previousStateData)\n\t\t\ttraverser = GraphTraverser.fromState(_initialBlueprint, state)\n\n\t\t\treturn state.toResult(runtime.serializer, undefined)\n\t\t},\n\t\tasync next(options: { signal?: AbortSignal; concurrency?: number } = {}) {\n\t\t\tif (stepper.isDone()) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst serializedContext = (await state.toResult(runtime.serializer, undefined)).serializedContext\n\t\t\thistory.push(serializedContext)\n\n\t\t\tconst executionContext = new ExecutionContext(\n\t\t\t\t_initialBlueprint,\n\t\t\t\tstate,\n\t\t\t\tnodeRegistry,\n\t\t\t\texecutionId,\n\t\t\t\truntime,\n\t\t\t\t{\n\t\t\t\t\tlogger: runtime.logger,\n\t\t\t\t\teventBus: runtime.eventBus,\n\t\t\t\t\tserializer: runtime.serializer,\n\t\t\t\t\tevaluator: runtime.evaluator,\n\t\t\t\t\tmiddleware: runtime.middleware,\n\t\t\t\t\tdependencies: runtime.dependencies,\n\t\t\t\t},\n\t\t\t\toptions.signal,\n\t\t\t\toptions.concurrency,\n\t\t\t)\n\t\t\treturn orchestrator.run(executionContext, traverser)\n\t\t},\n\t}\n\n\treturn stepper\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowcraft",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.8.
|
|
4
|
+
"version": "2.8.1",
|
|
5
5
|
"description": "A lightweight workflow engine",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://flowcraft.js.org",
|
|
@@ -27,34 +27,34 @@
|
|
|
27
27
|
],
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
30
|
-
"types": "./dist/index.d.
|
|
31
|
-
"import": "./dist/index.
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"import": "./dist/index.mjs"
|
|
32
32
|
},
|
|
33
33
|
"./sdk": {
|
|
34
|
-
"types": "./dist/sdk.d.
|
|
35
|
-
"import": "./dist/sdk.
|
|
34
|
+
"types": "./dist/sdk.d.mts",
|
|
35
|
+
"import": "./dist/sdk.mjs"
|
|
36
36
|
},
|
|
37
37
|
"./testing": {
|
|
38
|
-
"types": "./dist/testing/index.d.
|
|
39
|
-
"import": "./dist/testing/index.
|
|
38
|
+
"types": "./dist/testing/index.d.mts",
|
|
39
|
+
"import": "./dist/testing/index.mjs"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"dist"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"dev": "
|
|
47
|
-
"build": "
|
|
46
|
+
"dev": "tsdown --watch",
|
|
47
|
+
"build": "tsdown",
|
|
48
48
|
"typecheck": "tsc --noEmit",
|
|
49
49
|
"test": "vitest run",
|
|
50
50
|
"test:watch": "vitest",
|
|
51
51
|
"test:coverage": "vitest run --coverage"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@types/node": "^25.
|
|
54
|
+
"@types/node": "^25.5.0",
|
|
55
55
|
"@vitest/coverage-v8": "4.0.4",
|
|
56
|
-
"
|
|
57
|
-
"typescript": "^
|
|
58
|
-
"vitest": "^4.
|
|
56
|
+
"tsdown": "^0.21.7",
|
|
57
|
+
"typescript": "^6.0.2",
|
|
58
|
+
"vitest": "^4.1.2"
|
|
59
59
|
}
|
|
60
60
|
}
|
package/dist/adapters/index.d.ts
DELETED
package/dist/adapters/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { z as FlowcraftEvent, A as IEventBus } from '../types-Biip2gLh.js';
|
|
2
|
-
import '../errors.js';
|
|
3
|
-
import '../container.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Interface for a persistent storage mechanism for events.
|
|
7
|
-
* Implementations can store events in databases, log streams, files, etc.
|
|
8
|
-
*/
|
|
9
|
-
interface IEventStore {
|
|
10
|
-
/**
|
|
11
|
-
* Store an event persistently.
|
|
12
|
-
* @param event The event to store
|
|
13
|
-
* @param executionId The execution ID for grouping events
|
|
14
|
-
*/
|
|
15
|
-
store(event: FlowcraftEvent, executionId: string): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Retrieve all events for a specific execution.
|
|
18
|
-
* @param executionId The execution ID
|
|
19
|
-
* @returns Array of events in chronological order
|
|
20
|
-
*/
|
|
21
|
-
retrieve(executionId: string): Promise<FlowcraftEvent[]>;
|
|
22
|
-
/**
|
|
23
|
-
* Retrieve events for multiple executions.
|
|
24
|
-
* @param executionIds Array of execution IDs
|
|
25
|
-
* @returns Map of execution ID to array of events
|
|
26
|
-
*/
|
|
27
|
-
retrieveMultiple(executionIds: string[]): Promise<Map<string, FlowcraftEvent[]>>;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* A pluggable event bus adapter that persists all workflow events
|
|
31
|
-
* to a configurable storage backend, enabling time-travel debugging and replay.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* // Using a database-backed store
|
|
36
|
-
* const eventStore = new DatabaseEventStore(dbConnection)
|
|
37
|
-
* const eventBus = new PersistentEventBusAdapter(eventStore)
|
|
38
|
-
* const runtime = new FlowRuntime({ eventBus })
|
|
39
|
-
*
|
|
40
|
-
* // Later, replay the execution
|
|
41
|
-
* const events = await eventStore.retrieve(executionId)
|
|
42
|
-
* const finalState = await runtime.replay(blueprint, events)
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
declare class PersistentEventBusAdapter implements IEventBus {
|
|
46
|
-
private store;
|
|
47
|
-
constructor(store: IEventStore);
|
|
48
|
-
/**
|
|
49
|
-
* Emit an event by storing it persistently.
|
|
50
|
-
* Also emits to console for debugging (can be made configurable).
|
|
51
|
-
*/
|
|
52
|
-
emit(event: FlowcraftEvent): Promise<void>;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Simple in-memory event store for testing and development.
|
|
56
|
-
* Not suitable for production use.
|
|
57
|
-
*/
|
|
58
|
-
declare class InMemoryEventStore implements IEventStore {
|
|
59
|
-
private events;
|
|
60
|
-
store(event: FlowcraftEvent, executionId: string): Promise<void>;
|
|
61
|
-
retrieve(executionId: string): Promise<FlowcraftEvent[]>;
|
|
62
|
-
retrieveMultiple(executionIds: string[]): Promise<Map<string, FlowcraftEvent[]>>;
|
|
63
|
-
/**
|
|
64
|
-
* Clear all stored events (useful for testing).
|
|
65
|
-
*/
|
|
66
|
-
clear(): void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export { type IEventStore, InMemoryEventStore, PersistentEventBusAdapter };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"persistent-event-bus.js"}
|