@jagreehal/workflow 1.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/core.cjs +2 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +1606 -0
- package/dist/core.d.ts +1606 -0
- package/dist/core.js +2 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/workflow.cjs +2 -0
- package/dist/workflow.cjs.map +1 -0
- package/dist/workflow.d.cts +775 -0
- package/dist/workflow.d.ts +775 -0
- package/dist/workflow.js +2 -0
- package/dist/workflow.js.map +1 -0
- package/docs/___advanced.test.ts +565 -0
- package/docs/___advanced_VERIFICATION.md +64 -0
- package/docs/advanced.md +234 -0
- package/docs/api.md +195 -0
- package/package.json +119 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
import { Result, ErrorOf, CauseOf, UnexpectedError, WorkflowEvent, StepFailureMeta, RunStep, AsyncResult } from './core.cjs';
|
|
2
|
+
export { StepOptions } from './core.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @jagreehal/workflow/workflow
|
|
6
|
+
*
|
|
7
|
+
* Workflow orchestration with createWorkflow.
|
|
8
|
+
* Use this for typed async workflows with automatic error inference.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interface for step result caching.
|
|
13
|
+
* Implement this interface to provide custom caching strategies.
|
|
14
|
+
* A simple Map<string, Result> works for in-memory caching.
|
|
15
|
+
*
|
|
16
|
+
* Note: Cache stores Result<unknown, unknown, unknown> because different steps
|
|
17
|
+
* have different value/error/cause types. The actual runtime values are preserved;
|
|
18
|
+
* only the static types are widened. For error results, the cause value is encoded
|
|
19
|
+
* in CachedErrorCause to preserve metadata for proper replay.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Simple in-memory cache
|
|
23
|
+
* const cache = new Map<string, Result<unknown, unknown, unknown>>();
|
|
24
|
+
*
|
|
25
|
+
* // Or implement custom cache with TTL, LRU, etc.
|
|
26
|
+
* const cache: StepCache = {
|
|
27
|
+
* get: (key) => myCache.get(key),
|
|
28
|
+
* set: (key, result) => myCache.set(key, result, { ttl: 60000 }),
|
|
29
|
+
* has: (key) => myCache.has(key),
|
|
30
|
+
* delete: (key) => myCache.delete(key),
|
|
31
|
+
* clear: () => myCache.clear(),
|
|
32
|
+
* };
|
|
33
|
+
*/
|
|
34
|
+
interface StepCache {
|
|
35
|
+
get(key: string): Result<unknown, unknown, unknown> | undefined;
|
|
36
|
+
set(key: string, result: Result<unknown, unknown, unknown>): void;
|
|
37
|
+
has(key: string): boolean;
|
|
38
|
+
delete(key: string): boolean;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Entry for a saved step result with optional metadata.
|
|
43
|
+
* The meta field preserves origin information for proper replay.
|
|
44
|
+
*/
|
|
45
|
+
interface ResumeStateEntry {
|
|
46
|
+
result: Result<unknown, unknown, unknown>;
|
|
47
|
+
/** Optional metadata for error origin (from step_complete event) */
|
|
48
|
+
meta?: StepFailureMeta;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resume state for workflow replay.
|
|
52
|
+
* Pre-populate step results to skip execution on resume.
|
|
53
|
+
*
|
|
54
|
+
* Note: When saving to persistent storage, you may need custom serialization
|
|
55
|
+
* for complex cause types. JSON.stringify works for simple values, but Error
|
|
56
|
+
* objects and other non-plain types require special handling.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // Collect from step_complete events using the helper
|
|
60
|
+
* const collector = createStepCollector();
|
|
61
|
+
* const workflow = createWorkflow({ fetchUser }, {
|
|
62
|
+
* onEvent: collector.handleEvent,
|
|
63
|
+
* });
|
|
64
|
+
* // Later: collector.getState() returns ResumeState
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // Resume with saved state
|
|
68
|
+
* const workflow = createWorkflow({ fetchUser }, {
|
|
69
|
+
* resumeState: { steps: savedSteps }
|
|
70
|
+
* });
|
|
71
|
+
*/
|
|
72
|
+
interface ResumeState {
|
|
73
|
+
/** Map of step keys to their cached results with optional metadata */
|
|
74
|
+
steps: Map<string, ResumeStateEntry>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a collector for step results to build resume state.
|
|
78
|
+
*
|
|
79
|
+
* ## When to Use
|
|
80
|
+
*
|
|
81
|
+
* Use `createStepCollector` when you need to:
|
|
82
|
+
* - **Save workflow state** for later replay/resume
|
|
83
|
+
* - **Persist step results** to a database or file system
|
|
84
|
+
* - **Build resume state** from workflow execution
|
|
85
|
+
* - **Enable workflow replay** after application restarts
|
|
86
|
+
*
|
|
87
|
+
* ## Why Use This Instead of Manual Collection
|
|
88
|
+
*
|
|
89
|
+
* - **Automatic filtering**: Only collects `step_complete` events (ignores other events)
|
|
90
|
+
* - **Metadata preservation**: Captures both result and meta for proper error replay
|
|
91
|
+
* - **Type-safe**: Returns properly typed `ResumeState`
|
|
92
|
+
* - **Convenient API**: Simple `handleEvent` → `getState` pattern
|
|
93
|
+
*
|
|
94
|
+
* ## How It Works
|
|
95
|
+
*
|
|
96
|
+
* 1. Create collector and pass `handleEvent` to workflow's `onEvent` option
|
|
97
|
+
* 2. Workflow emits `step_complete` events for keyed steps
|
|
98
|
+
* 3. Collector automatically captures these events
|
|
99
|
+
* 4. Call `getState()` to get the collected `ResumeState`
|
|
100
|
+
* 5. Persist state (e.g., to database) for later resume
|
|
101
|
+
*
|
|
102
|
+
* ## Important Notes
|
|
103
|
+
*
|
|
104
|
+
* - Only steps with a `key` option are collected (unkeyed steps are not saved)
|
|
105
|
+
* - The collector preserves error metadata for proper replay behavior
|
|
106
|
+
* - State can be serialized to JSON (but complex cause types may need custom handling)
|
|
107
|
+
*
|
|
108
|
+
* @returns An object with:
|
|
109
|
+
* - `handleEvent`: Function to pass to workflow's `onEvent` option
|
|
110
|
+
* - `getState`: Get collected resume state (call after workflow execution)
|
|
111
|
+
* - `clear`: Clear all collected state
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // Collect state during workflow execution
|
|
116
|
+
* const collector = createStepCollector();
|
|
117
|
+
*
|
|
118
|
+
* const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
119
|
+
* onEvent: collector.handleEvent, // Pass collector's handler
|
|
120
|
+
* });
|
|
121
|
+
*
|
|
122
|
+
* await workflow(async (step) => {
|
|
123
|
+
* // Only keyed steps are collected
|
|
124
|
+
* const user = await step(() => fetchUser("1"), { key: "user:1" });
|
|
125
|
+
* const posts = await step(() => fetchPosts(user.id), { key: `posts:${user.id}` });
|
|
126
|
+
* return { user, posts };
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* // Get collected state for persistence
|
|
130
|
+
* const state = collector.getState();
|
|
131
|
+
* // state.steps contains: 'user:1' and 'posts:1' entries
|
|
132
|
+
*
|
|
133
|
+
* // Save to database
|
|
134
|
+
* await db.saveWorkflowState(workflowId, state);
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Resume workflow from saved state
|
|
140
|
+
* const savedState = await db.loadWorkflowState(workflowId);
|
|
141
|
+
* const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
142
|
+
* resumeState: savedState // Pre-populate cache from saved state
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* // Cached steps skip execution, new steps run normally
|
|
146
|
+
* await workflow(async (step) => {
|
|
147
|
+
* const user = await step(() => fetchUser("1"), { key: "user:1" }); // Cache hit
|
|
148
|
+
* const posts = await step(() => fetchPosts(user.id), { key: `posts:${user.id}` }); // Cache hit
|
|
149
|
+
* return { user, posts };
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
declare function createStepCollector(): {
|
|
154
|
+
handleEvent: (event: WorkflowEvent<unknown>) => void;
|
|
155
|
+
getState: () => ResumeState;
|
|
156
|
+
clear: () => void;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Constraint for Result-returning functions
|
|
160
|
+
* Used by createWorkflow to ensure only valid functions are passed
|
|
161
|
+
*/
|
|
162
|
+
type AnyResultFn = (...args: any[]) => Result<any, any, any> | Promise<Result<any, any, any>>;
|
|
163
|
+
/**
|
|
164
|
+
* Extract union of error types from a deps object
|
|
165
|
+
* Example: ErrorsOfDeps<{ fetchUser: typeof fetchUser, fetchPosts: typeof fetchPosts }>
|
|
166
|
+
* yields: 'NOT_FOUND' | 'FETCH_ERROR'
|
|
167
|
+
*/
|
|
168
|
+
type ErrorsOfDeps<Deps extends Record<string, AnyResultFn>> = ErrorOf<Deps[keyof Deps]>;
|
|
169
|
+
/**
|
|
170
|
+
* Extract union of cause types from a deps object.
|
|
171
|
+
* Example: CausesOfDeps<{ fetchUser: typeof fetchUser }> where fetchUser returns Result<User, "NOT_FOUND", Error>
|
|
172
|
+
* yields: Error
|
|
173
|
+
*
|
|
174
|
+
* Note: This represents the domain cause types from declared functions.
|
|
175
|
+
* However, workflow results may also have unknown causes from step.try failures
|
|
176
|
+
* or uncaught exceptions, so the actual Result cause type is `unknown`.
|
|
177
|
+
*/
|
|
178
|
+
type CausesOfDeps<Deps extends Record<string, AnyResultFn>> = CauseOf<Deps[keyof Deps]>;
|
|
179
|
+
/**
|
|
180
|
+
* Non-strict workflow options
|
|
181
|
+
* Returns E | UnexpectedError (safe default)
|
|
182
|
+
*/
|
|
183
|
+
type WorkflowOptions<E, C = void> = {
|
|
184
|
+
onError?: (error: E | UnexpectedError, stepName?: string) => void;
|
|
185
|
+
/** Unified event stream for workflow and step lifecycle */
|
|
186
|
+
onEvent?: (event: WorkflowEvent<E | UnexpectedError>, ctx: C) => void;
|
|
187
|
+
/** Create per-run context for event correlation */
|
|
188
|
+
createContext?: () => C;
|
|
189
|
+
/** Step result cache - only steps with a `key` option are cached */
|
|
190
|
+
cache?: StepCache;
|
|
191
|
+
/** Pre-populate cache from saved state for workflow resume */
|
|
192
|
+
resumeState?: ResumeState | (() => ResumeState | Promise<ResumeState>);
|
|
193
|
+
catchUnexpected?: never;
|
|
194
|
+
strict?: false;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Strict workflow options
|
|
198
|
+
* Returns E | U (closed error union)
|
|
199
|
+
*/
|
|
200
|
+
type WorkflowOptionsStrict<E, U, C = void> = {
|
|
201
|
+
strict: true;
|
|
202
|
+
catchUnexpected: (cause: unknown) => U;
|
|
203
|
+
onError?: (error: E | U, stepName?: string) => void;
|
|
204
|
+
/** Unified event stream for workflow and step lifecycle */
|
|
205
|
+
onEvent?: (event: WorkflowEvent<E | U>, ctx: C) => void;
|
|
206
|
+
/** Create per-run context for event correlation */
|
|
207
|
+
createContext?: () => C;
|
|
208
|
+
/** Step result cache - only steps with a `key` option are cached */
|
|
209
|
+
cache?: StepCache;
|
|
210
|
+
/** Pre-populate cache from saved state for workflow resume */
|
|
211
|
+
resumeState?: ResumeState | (() => ResumeState | Promise<ResumeState>);
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Workflow return type (non-strict)
|
|
215
|
+
* Supports both argument-less and argument-passing call patterns
|
|
216
|
+
*
|
|
217
|
+
* Note: Cause type is `unknown` because:
|
|
218
|
+
* - step.try errors have thrown values as cause
|
|
219
|
+
* - Uncaught exceptions produce unknown causes
|
|
220
|
+
* - Different steps may have different cause types
|
|
221
|
+
* The cause IS preserved at runtime; narrow based on error type if needed.
|
|
222
|
+
*/
|
|
223
|
+
interface Workflow<E, Deps> {
|
|
224
|
+
/**
|
|
225
|
+
* Execute workflow without arguments (original API)
|
|
226
|
+
*/
|
|
227
|
+
<T>(fn: (step: RunStep<E>, deps: Deps) => T | Promise<T>): AsyncResult<T, E | UnexpectedError, unknown>;
|
|
228
|
+
/**
|
|
229
|
+
* Execute workflow with typed arguments
|
|
230
|
+
* @param args - Typed arguments passed to the callback (type inferred at call site)
|
|
231
|
+
* @param fn - Callback receives (step, deps, args)
|
|
232
|
+
*/
|
|
233
|
+
<T, Args>(args: Args, fn: (step: RunStep<E>, deps: Deps, args: Args) => T | Promise<T>): AsyncResult<T, E | UnexpectedError, unknown>;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Workflow return type (strict)
|
|
237
|
+
* Supports both argument-less and argument-passing call patterns
|
|
238
|
+
*
|
|
239
|
+
* Note: Cause type is `unknown` because catchUnexpected receives thrown
|
|
240
|
+
* values which have unknown type.
|
|
241
|
+
*/
|
|
242
|
+
interface WorkflowStrict<E, U, Deps> {
|
|
243
|
+
/**
|
|
244
|
+
* Execute workflow without arguments (original API)
|
|
245
|
+
*/
|
|
246
|
+
<T>(fn: (step: RunStep<E>, deps: Deps) => T | Promise<T>): AsyncResult<T, E | U, unknown>;
|
|
247
|
+
/**
|
|
248
|
+
* Execute workflow with typed arguments
|
|
249
|
+
* @param args - Typed arguments passed to the callback (type inferred at call site)
|
|
250
|
+
* @param fn - Callback receives (step, deps, args)
|
|
251
|
+
*/
|
|
252
|
+
<T, Args>(args: Args, fn: (step: RunStep<E>, deps: Deps, args: Args) => T | Promise<T>): AsyncResult<T, E | U, unknown>;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Create a typed workflow with automatic error inference.
|
|
256
|
+
*
|
|
257
|
+
* ## When to Use `createWorkflow`
|
|
258
|
+
*
|
|
259
|
+
* Use `createWorkflow` when you have:
|
|
260
|
+
* - **Multiple dependent async operations** that need to run sequentially
|
|
261
|
+
* - **Complex error handling** where you want type-safe error unions
|
|
262
|
+
* - **Need for observability** via event streams (onEvent)
|
|
263
|
+
* - **Step caching** requirements for expensive operations
|
|
264
|
+
* - **Resume/replay** capabilities for long-running workflows
|
|
265
|
+
* - **Human-in-the-loop** workflows requiring approvals
|
|
266
|
+
*
|
|
267
|
+
* ## Why Use `createWorkflow` Instead of `run()`
|
|
268
|
+
*
|
|
269
|
+
* 1. **Automatic Error Type Inference**: Errors are computed from your declared functions
|
|
270
|
+
* - No manual error union management
|
|
271
|
+
* - TypeScript ensures all possible errors are handled
|
|
272
|
+
* - Refactoring is safer - adding/removing functions updates error types automatically
|
|
273
|
+
*
|
|
274
|
+
* 2. **Step Caching**: Expensive operations can be cached by key
|
|
275
|
+
* - Prevents duplicate API calls
|
|
276
|
+
* - Useful for idempotent operations
|
|
277
|
+
* - Supports resume state for workflow replay
|
|
278
|
+
*
|
|
279
|
+
* 3. **Event Stream**: Built-in observability via `onEvent`
|
|
280
|
+
* - Track workflow and step lifecycle
|
|
281
|
+
* - Monitor performance (durationMs)
|
|
282
|
+
* - Build dashboards and debugging tools
|
|
283
|
+
*
|
|
284
|
+
* 4. **Resume State**: Save and replay workflows
|
|
285
|
+
* - Useful for long-running processes
|
|
286
|
+
* - Supports human-in-the-loop workflows
|
|
287
|
+
* - Enables workflow persistence across restarts
|
|
288
|
+
*
|
|
289
|
+
* ## How It Works
|
|
290
|
+
*
|
|
291
|
+
* 1. **Declare Dependencies**: Pass an object of Result-returning functions
|
|
292
|
+
* 2. **Automatic Inference**: Error types are extracted from function return types
|
|
293
|
+
* 3. **Execute Workflow**: Call the returned workflow function with your logic
|
|
294
|
+
* 4. **Early Exit**: `step()` unwraps Results - on error, workflow exits immediately
|
|
295
|
+
*
|
|
296
|
+
* ## Error Type Inference
|
|
297
|
+
*
|
|
298
|
+
* The error union is automatically computed from all declared functions:
|
|
299
|
+
* - Each function's error type is extracted
|
|
300
|
+
* - Union of all errors is created
|
|
301
|
+
* - `UnexpectedError` is added for uncaught exceptions (unless strict mode)
|
|
302
|
+
*
|
|
303
|
+
* ## Strict Mode
|
|
304
|
+
*
|
|
305
|
+
* Use `strict: true` with `catchUnexpected` for closed error unions:
|
|
306
|
+
* - Removes `UnexpectedError` from the union
|
|
307
|
+
* - All errors must be explicitly handled
|
|
308
|
+
* - Useful for production code where you want exhaustive error handling
|
|
309
|
+
*
|
|
310
|
+
* @param deps - Object mapping names to Result-returning functions.
|
|
311
|
+
* These functions must return `Result<T, E>` or `Promise<Result<T, E>>`.
|
|
312
|
+
* The error types (`E`) from all functions are automatically combined into a union.
|
|
313
|
+
* @param options - Optional configuration:
|
|
314
|
+
* - `onEvent`: Callback for workflow/step lifecycle events
|
|
315
|
+
* - `onError`: Callback for error logging/debugging
|
|
316
|
+
* - `cache`: Step result cache (Map or custom StepCache implementation)
|
|
317
|
+
* - `resumeState`: Pre-populated step results for workflow replay
|
|
318
|
+
* - `createContext`: Factory for per-run context (passed to onEvent)
|
|
319
|
+
* - `strict`: Enable strict mode (requires `catchUnexpected`)
|
|
320
|
+
* - `catchUnexpected`: Map uncaught exceptions to typed errors (required in strict mode)
|
|
321
|
+
*
|
|
322
|
+
* @returns A workflow function that accepts your workflow logic and returns an AsyncResult.
|
|
323
|
+
* The error type is automatically inferred from the `deps` parameter.
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* // Basic usage - automatic error inference
|
|
328
|
+
* const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
|
|
329
|
+
* id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
|
|
330
|
+
*
|
|
331
|
+
* const fetchPosts = async (userId: string): AsyncResult<Post[], 'FETCH_ERROR'> =>
|
|
332
|
+
* ok([{ id: 1, title: 'Hello' }]);
|
|
333
|
+
*
|
|
334
|
+
* const getPosts = createWorkflow({ fetchUser, fetchPosts });
|
|
335
|
+
*
|
|
336
|
+
* const result = await getPosts(async (step) => {
|
|
337
|
+
* const user = await step(fetchUser('1'));
|
|
338
|
+
* const posts = await step(fetchPosts(user.id));
|
|
339
|
+
* return { user, posts };
|
|
340
|
+
* });
|
|
341
|
+
* // result.error: 'NOT_FOUND' | 'FETCH_ERROR' | UnexpectedError
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* // With destructuring in callback (optional but convenient)
|
|
347
|
+
* const result = await getPosts(async (step, { fetchUser, fetchPosts }) => {
|
|
348
|
+
* const user = await step(fetchUser('1'));
|
|
349
|
+
* const posts = await step(fetchPosts(user.id));
|
|
350
|
+
* return { user, posts };
|
|
351
|
+
* });
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```typescript
|
|
356
|
+
* // Strict mode - closed error union (no UnexpectedError)
|
|
357
|
+
* const getPosts = createWorkflow(
|
|
358
|
+
* { fetchUser, fetchPosts },
|
|
359
|
+
* {
|
|
360
|
+
* strict: true,
|
|
361
|
+
* catchUnexpected: () => 'UNEXPECTED' as const
|
|
362
|
+
* }
|
|
363
|
+
* );
|
|
364
|
+
* // result.error: 'NOT_FOUND' | 'FETCH_ERROR' | 'UNEXPECTED' (exactly)
|
|
365
|
+
* ```
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* // With step caching
|
|
370
|
+
* const cache = new Map<string, Result<unknown, unknown>>();
|
|
371
|
+
* const workflow = createWorkflow({ fetchUser }, { cache });
|
|
372
|
+
*
|
|
373
|
+
* const result = await workflow(async (step) => {
|
|
374
|
+
* // First call executes fetchUser
|
|
375
|
+
* const user1 = await step(() => fetchUser('1'), { key: 'user:1' });
|
|
376
|
+
* // Second call with same key uses cache (fetchUser not called again)
|
|
377
|
+
* const user2 = await step(() => fetchUser('1'), { key: 'user:1' });
|
|
378
|
+
* return user1; // user1 === user2
|
|
379
|
+
* });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```typescript
|
|
384
|
+
* // With event stream for observability
|
|
385
|
+
* const workflow = createWorkflow({ fetchUser }, {
|
|
386
|
+
* onEvent: (event) => {
|
|
387
|
+
* if (event.type === 'step_start') {
|
|
388
|
+
* console.log(`Step ${event.name} started`);
|
|
389
|
+
* }
|
|
390
|
+
* if (event.type === 'step_success') {
|
|
391
|
+
* console.log(`Step ${event.name} completed in ${event.durationMs}ms`);
|
|
392
|
+
* }
|
|
393
|
+
* }
|
|
394
|
+
* });
|
|
395
|
+
* ```
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* // With resume state for workflow replay
|
|
400
|
+
* const savedState = { steps: new Map([['user:1', { result: ok({ id: '1', name: 'Alice' }) }]]) };
|
|
401
|
+
* const workflow = createWorkflow({ fetchUser }, { resumeState: savedState });
|
|
402
|
+
*
|
|
403
|
+
* const result = await workflow(async (step) => {
|
|
404
|
+
* // This step uses cached result from savedState (fetchUser not called)
|
|
405
|
+
* const user = await step(() => fetchUser('1'), { key: 'user:1' });
|
|
406
|
+
* return user;
|
|
407
|
+
* });
|
|
408
|
+
* ```
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* // With typed arguments (new API)
|
|
413
|
+
* const workflow = createWorkflow({ fetchUser, fetchPosts });
|
|
414
|
+
*
|
|
415
|
+
* const result = await workflow(
|
|
416
|
+
* { userId: '1' }, // Typed arguments
|
|
417
|
+
* async (step, { fetchUser, fetchPosts }, { userId }) => {
|
|
418
|
+
* const user = await step(fetchUser(userId));
|
|
419
|
+
* const posts = await step(fetchPosts(user.id));
|
|
420
|
+
* return { user, posts };
|
|
421
|
+
* }
|
|
422
|
+
* );
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
declare function createWorkflow<const Deps extends Readonly<Record<string, AnyResultFn>>, C = void>(deps: Deps, options?: WorkflowOptions<ErrorsOfDeps<Deps>, C>): Workflow<ErrorsOfDeps<Deps>, Deps>;
|
|
426
|
+
declare function createWorkflow<const Deps extends Readonly<Record<string, AnyResultFn>>, U, C = void>(deps: Deps, options: WorkflowOptionsStrict<ErrorsOfDeps<Deps>, U, C>): WorkflowStrict<ErrorsOfDeps<Deps>, U, Deps>;
|
|
427
|
+
/**
|
|
428
|
+
* Type guard to check if an event is a step_complete event.
|
|
429
|
+
* Use this to filter events for state persistence.
|
|
430
|
+
*
|
|
431
|
+
* @param event - The workflow event to check
|
|
432
|
+
* @returns `true` if the event is a step_complete event, `false` otherwise
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```typescript
|
|
436
|
+
* const savedSteps = new Map<string, Result<unknown, unknown>>();
|
|
437
|
+
*
|
|
438
|
+
* const workflow = createWorkflow({ fetchUser }, {
|
|
439
|
+
* onEvent: (event) => {
|
|
440
|
+
* if (isStepComplete(event)) {
|
|
441
|
+
* savedSteps.set(event.stepKey, event.result);
|
|
442
|
+
* }
|
|
443
|
+
* }
|
|
444
|
+
* });
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
declare function isStepComplete(event: WorkflowEvent<unknown>): event is Extract<WorkflowEvent<unknown>, {
|
|
448
|
+
type: "step_complete";
|
|
449
|
+
}>;
|
|
450
|
+
/**
|
|
451
|
+
* Standard error type for steps awaiting human approval.
|
|
452
|
+
* Use this as the error type for approval-gated steps.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* const requireApproval = async (userId: string): AsyncResult<Approval, PendingApproval> => {
|
|
456
|
+
* const status = await checkApprovalStatus(userId);
|
|
457
|
+
* if (status === 'pending') {
|
|
458
|
+
* return err({ type: 'PENDING_APPROVAL', stepKey: `approval:${userId}` });
|
|
459
|
+
* }
|
|
460
|
+
* return ok(status.approval);
|
|
461
|
+
* };
|
|
462
|
+
*/
|
|
463
|
+
type PendingApproval = {
|
|
464
|
+
type: "PENDING_APPROVAL";
|
|
465
|
+
/** Step key for correlation when resuming */
|
|
466
|
+
stepKey: string;
|
|
467
|
+
/** Optional reason for the pending state */
|
|
468
|
+
reason?: string;
|
|
469
|
+
/** Optional metadata for the approval request */
|
|
470
|
+
metadata?: Record<string, unknown>;
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* Error returned when approval is rejected.
|
|
474
|
+
*/
|
|
475
|
+
type ApprovalRejected = {
|
|
476
|
+
type: "APPROVAL_REJECTED";
|
|
477
|
+
/** Step key for correlation */
|
|
478
|
+
stepKey: string;
|
|
479
|
+
/** Reason the approval was rejected */
|
|
480
|
+
reason: string;
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Type guard to check if an error is a PendingApproval.
|
|
484
|
+
*
|
|
485
|
+
* @param error - The error to check
|
|
486
|
+
* @returns `true` if the error is a PendingApproval, `false` otherwise
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* ```typescript
|
|
490
|
+
* const result = await workflow(...);
|
|
491
|
+
* if (!result.ok && isPendingApproval(result.error)) {
|
|
492
|
+
* console.log(`Waiting for approval: ${result.error.stepKey}`);
|
|
493
|
+
* }
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
declare function isPendingApproval(error: unknown): error is PendingApproval;
|
|
497
|
+
/**
|
|
498
|
+
* Type guard to check if an error is an ApprovalRejected.
|
|
499
|
+
*
|
|
500
|
+
* @param error - The error to check
|
|
501
|
+
* @returns `true` if the error is an ApprovalRejected, `false` otherwise
|
|
502
|
+
*/
|
|
503
|
+
declare function isApprovalRejected(error: unknown): error is ApprovalRejected;
|
|
504
|
+
/**
|
|
505
|
+
* Create a PendingApproval error result.
|
|
506
|
+
* Convenience helper for approval-gated steps.
|
|
507
|
+
*
|
|
508
|
+
* @param stepKey - Stable key for this approval step (used for resume)
|
|
509
|
+
* @param options - Optional reason and metadata for the pending approval
|
|
510
|
+
* @returns A Result with a PendingApproval error
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```typescript
|
|
514
|
+
* const requireApproval = async (userId: string) => {
|
|
515
|
+
* const status = await db.getApproval(userId);
|
|
516
|
+
* if (!status) return pendingApproval(`approval:${userId}`);
|
|
517
|
+
* return ok(status);
|
|
518
|
+
* };
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
declare function pendingApproval(stepKey: string, options?: {
|
|
522
|
+
reason?: string;
|
|
523
|
+
metadata?: Record<string, unknown>;
|
|
524
|
+
}): Result<never, PendingApproval>;
|
|
525
|
+
/**
|
|
526
|
+
* Options for creating an approval-gated step.
|
|
527
|
+
*/
|
|
528
|
+
interface ApprovalStepOptions<T> {
|
|
529
|
+
/** Stable key for this approval step (used for resume) */
|
|
530
|
+
key: string;
|
|
531
|
+
/** Function to check current approval status from external source */
|
|
532
|
+
checkApproval: () => Promise<{
|
|
533
|
+
status: "pending";
|
|
534
|
+
} | {
|
|
535
|
+
status: "approved";
|
|
536
|
+
value: T;
|
|
537
|
+
} | {
|
|
538
|
+
status: "rejected";
|
|
539
|
+
reason: string;
|
|
540
|
+
}>;
|
|
541
|
+
/** Optional reason shown when pending */
|
|
542
|
+
pendingReason?: string;
|
|
543
|
+
/** Optional metadata for the approval request */
|
|
544
|
+
metadata?: Record<string, unknown>;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Create a Result-returning function that checks external approval status.
|
|
548
|
+
*
|
|
549
|
+
* ## When to Use
|
|
550
|
+
*
|
|
551
|
+
* Use `createApprovalStep` when you need:
|
|
552
|
+
* - **Human-in-the-loop workflows**: Steps that require human approval
|
|
553
|
+
* - **External approval systems**: Integrate with approval databases/APIs
|
|
554
|
+
* - **Workflow pausing**: Workflows that pause and resume after approval
|
|
555
|
+
* - **Approval tracking**: Track who approved what and when
|
|
556
|
+
*
|
|
557
|
+
* ## Why Use This Instead of Manual Approval Checks
|
|
558
|
+
*
|
|
559
|
+
* - **Standardized pattern**: Consistent approval step interface
|
|
560
|
+
* - **Type-safe**: Returns typed `PendingApproval` or `ApprovalRejected` errors
|
|
561
|
+
* - **Resume-friendly**: Works seamlessly with `injectApproval()` and resume state
|
|
562
|
+
* - **Metadata support**: Can include approval reason and metadata
|
|
563
|
+
*
|
|
564
|
+
* ## How It Works
|
|
565
|
+
*
|
|
566
|
+
* 1. Create approval step with `checkApproval` function
|
|
567
|
+
* 2. `checkApproval` returns one of:
|
|
568
|
+
* - `{ status: 'pending' }` - Approval not yet granted (workflow pauses)
|
|
569
|
+
* - `{ status: 'approved', value: T }` - Approval granted (workflow continues)
|
|
570
|
+
* - `{ status: 'rejected', reason: string }` - Approval rejected (workflow fails)
|
|
571
|
+
* 3. Use in workflow with `step()` - workflow pauses if pending
|
|
572
|
+
* 4. When approval granted externally, use `injectApproval()` to resume
|
|
573
|
+
*
|
|
574
|
+
* ## Typical Approval Flow
|
|
575
|
+
*
|
|
576
|
+
* 1. Workflow executes → reaches approval step
|
|
577
|
+
* 2. `checkApproval()` called → returns `{ status: 'pending' }`
|
|
578
|
+
* 3. Workflow returns `PendingApproval` error
|
|
579
|
+
* 4. Save workflow state → persist for later resume
|
|
580
|
+
* 5. Show approval UI → user sees pending approval
|
|
581
|
+
* 6. User grants/rejects → update approval system
|
|
582
|
+
* 7. Inject approval → call `injectApproval()` with approved value
|
|
583
|
+
* 8. Resume workflow → continue from approval step
|
|
584
|
+
*
|
|
585
|
+
* @param options - Configuration for the approval step:
|
|
586
|
+
* - `key`: Stable key for this approval (must match step key in workflow)
|
|
587
|
+
* - `checkApproval`: Async function that checks current approval status
|
|
588
|
+
* - `pendingReason`: Optional reason shown when approval is pending
|
|
589
|
+
* - `metadata`: Optional metadata attached to the approval request
|
|
590
|
+
*
|
|
591
|
+
* @returns A function that returns an AsyncResult checking approval status.
|
|
592
|
+
* The function can be used directly with `step()` in workflows.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```typescript
|
|
596
|
+
* // Create approval step that checks database
|
|
597
|
+
* const requireManagerApproval = createApprovalStep<{ approvedBy: string }>({
|
|
598
|
+
* key: 'manager-approval',
|
|
599
|
+
* checkApproval: async () => {
|
|
600
|
+
* const approval = await db.getApproval('manager-approval');
|
|
601
|
+
* if (!approval) {
|
|
602
|
+
* return { status: 'pending' }; // Workflow pauses here
|
|
603
|
+
* }
|
|
604
|
+
* if (approval.rejected) {
|
|
605
|
+
* return { status: 'rejected', reason: approval.reason };
|
|
606
|
+
* }
|
|
607
|
+
* return {
|
|
608
|
+
* status: 'approved',
|
|
609
|
+
* value: { approvedBy: approval.approvedBy }
|
|
610
|
+
* };
|
|
611
|
+
* },
|
|
612
|
+
* pendingReason: 'Waiting for manager approval',
|
|
613
|
+
* });
|
|
614
|
+
*
|
|
615
|
+
* // Use in workflow
|
|
616
|
+
* const workflow = createWorkflow({ requireManagerApproval });
|
|
617
|
+
* const result = await workflow(async (step) => {
|
|
618
|
+
* const approval = await step(requireManagerApproval, { key: 'manager-approval' });
|
|
619
|
+
* // If pending, workflow exits with PendingApproval error
|
|
620
|
+
* // If approved, continues with approval value
|
|
621
|
+
* return approval;
|
|
622
|
+
* });
|
|
623
|
+
*
|
|
624
|
+
* // Handle pending state
|
|
625
|
+
* if (!result.ok && isPendingApproval(result.error)) {
|
|
626
|
+
* // Workflow paused - show approval UI
|
|
627
|
+
* showApprovalUI(result.error.stepKey);
|
|
628
|
+
* }
|
|
629
|
+
* ```
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```typescript
|
|
633
|
+
* // With approval injection for resume
|
|
634
|
+
* const collector = createHITLCollector();
|
|
635
|
+
* const workflow = createWorkflow({ requireApproval }, {
|
|
636
|
+
* onEvent: collector.handleEvent,
|
|
637
|
+
* });
|
|
638
|
+
*
|
|
639
|
+
* const result = await workflow(async (step) => {
|
|
640
|
+
* const approval = await step(requireApproval, { key: 'approval:1' });
|
|
641
|
+
* return approval;
|
|
642
|
+
* });
|
|
643
|
+
*
|
|
644
|
+
* // When approval granted externally
|
|
645
|
+
* if (collector.hasPendingApprovals()) {
|
|
646
|
+
* const resumeState = collector.injectApproval('approval:1', {
|
|
647
|
+
* approvedBy: 'admin@example.com'
|
|
648
|
+
* });
|
|
649
|
+
*
|
|
650
|
+
* // Resume workflow
|
|
651
|
+
* const workflow2 = createWorkflow({ requireApproval }, { resumeState });
|
|
652
|
+
* const result2 = await workflow2(async (step) => {
|
|
653
|
+
* const approval = await step(requireApproval, { key: 'approval:1' });
|
|
654
|
+
* return approval; // Now succeeds with injected value
|
|
655
|
+
* });
|
|
656
|
+
* }
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
declare function createApprovalStep<T>(options: ApprovalStepOptions<T>): () => AsyncResult<T, PendingApproval | ApprovalRejected>;
|
|
660
|
+
/**
|
|
661
|
+
* Inject an approved value into resume state.
|
|
662
|
+
* Use this when an external approval is granted and you want to resume the workflow.
|
|
663
|
+
*
|
|
664
|
+
* @param state - The resume state to update
|
|
665
|
+
* @param options - Object with stepKey and the approved value
|
|
666
|
+
* @returns A new ResumeState with the approval injected
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* ```typescript
|
|
670
|
+
* // When approval is granted externally:
|
|
671
|
+
* const updatedState = injectApproval(savedState, {
|
|
672
|
+
* stepKey: 'deploy:prod',
|
|
673
|
+
* value: { approvedBy: 'admin', approvedAt: Date.now() }
|
|
674
|
+
* });
|
|
675
|
+
*
|
|
676
|
+
* // Resume workflow with the approval injected
|
|
677
|
+
* const workflow = createWorkflow({ ... }, { resumeState: updatedState });
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
declare function injectApproval<T>(state: ResumeState, options: {
|
|
681
|
+
stepKey: string;
|
|
682
|
+
value: T;
|
|
683
|
+
}): ResumeState;
|
|
684
|
+
/**
|
|
685
|
+
* Remove a step from resume state (e.g., to force re-execution).
|
|
686
|
+
*
|
|
687
|
+
* @param state - The resume state to update
|
|
688
|
+
* @param stepKey - The key of the step to remove
|
|
689
|
+
* @returns A new ResumeState with the step removed
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```typescript
|
|
693
|
+
* // Force a step to re-execute on resume
|
|
694
|
+
* const updatedState = clearStep(savedState, 'approval:123');
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
697
|
+
declare function clearStep(state: ResumeState, stepKey: string): ResumeState;
|
|
698
|
+
/**
|
|
699
|
+
* Check if a step in resume state has a pending approval error.
|
|
700
|
+
*
|
|
701
|
+
* @param state - The resume state to check
|
|
702
|
+
* @param stepKey - The key of the step to check
|
|
703
|
+
* @returns `true` if the step has a pending approval, `false` otherwise
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```typescript
|
|
707
|
+
* if (hasPendingApproval(savedState, 'deploy:prod')) {
|
|
708
|
+
* // Show approval UI
|
|
709
|
+
* }
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
declare function hasPendingApproval(state: ResumeState, stepKey: string): boolean;
|
|
713
|
+
/**
|
|
714
|
+
* Get all pending approval step keys from resume state.
|
|
715
|
+
*
|
|
716
|
+
* @param state - The resume state to check
|
|
717
|
+
* @returns Array of step keys that have pending approvals
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```typescript
|
|
721
|
+
* const pendingKeys = getPendingApprovals(savedState);
|
|
722
|
+
* // ['deploy:prod', 'deploy:staging']
|
|
723
|
+
* ```
|
|
724
|
+
*/
|
|
725
|
+
declare function getPendingApprovals(state: ResumeState): string[];
|
|
726
|
+
/**
|
|
727
|
+
* Extended step collector that tracks pending approvals.
|
|
728
|
+
* Use this for HITL workflows that need to track approval state.
|
|
729
|
+
*
|
|
730
|
+
* @returns An object with methods to handle events, get state, and manage approvals
|
|
731
|
+
*
|
|
732
|
+
* @example
|
|
733
|
+
* ```typescript
|
|
734
|
+
* const collector = createHITLCollector();
|
|
735
|
+
*
|
|
736
|
+
* const workflow = createWorkflow({ fetchUser, requireApproval }, {
|
|
737
|
+
* onEvent: collector.handleEvent,
|
|
738
|
+
* });
|
|
739
|
+
*
|
|
740
|
+
* const result = await workflow(async (step) => {
|
|
741
|
+
* const user = await step(() => fetchUser("1"), { key: "user:1" });
|
|
742
|
+
* const approval = await step(requireApproval, { key: "approval:1" });
|
|
743
|
+
* return { user, approval };
|
|
744
|
+
* });
|
|
745
|
+
*
|
|
746
|
+
* // Check for pending approvals
|
|
747
|
+
* if (collector.hasPendingApprovals()) {
|
|
748
|
+
* const pending = collector.getPendingApprovals();
|
|
749
|
+
* // pending: [{ stepKey: 'approval:1', error: PendingApproval }]
|
|
750
|
+
* await saveToDatabase(collector.getState());
|
|
751
|
+
* }
|
|
752
|
+
*
|
|
753
|
+
* // Later, when approved:
|
|
754
|
+
* const resumeState = collector.injectApproval('approval:1', { approvedBy: 'admin' });
|
|
755
|
+
* ```
|
|
756
|
+
*/
|
|
757
|
+
declare function createHITLCollector(): {
|
|
758
|
+
/** Handle workflow events (pass to onEvent option) */
|
|
759
|
+
handleEvent: (event: WorkflowEvent<unknown>) => void;
|
|
760
|
+
/** Get collected resume state */
|
|
761
|
+
getState: () => ResumeState;
|
|
762
|
+
/** Clear all collected state */
|
|
763
|
+
clear: () => void;
|
|
764
|
+
/** Check if any steps have pending approvals */
|
|
765
|
+
hasPendingApprovals: () => boolean;
|
|
766
|
+
/** Get all pending approval entries with their errors */
|
|
767
|
+
getPendingApprovals: () => Array<{
|
|
768
|
+
stepKey: string;
|
|
769
|
+
error: PendingApproval;
|
|
770
|
+
}>;
|
|
771
|
+
/** Inject an approval result, updating the collector's internal state. Returns a copy for use as resumeState. */
|
|
772
|
+
injectApproval: <T>(stepKey: string, value: T) => ResumeState;
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
export { type AnyResultFn, type ApprovalRejected, type ApprovalStepOptions, AsyncResult, type CausesOfDeps, type ErrorsOfDeps, type PendingApproval, Result, type ResumeState, type ResumeStateEntry, RunStep, type StepCache, UnexpectedError, type Workflow, WorkflowEvent, type WorkflowOptions, type WorkflowOptionsStrict, type WorkflowStrict, clearStep, createApprovalStep, createHITLCollector, createStepCollector, createWorkflow, getPendingApprovals, hasPendingApproval, injectApproval, isApprovalRejected, isPendingApproval, isStepComplete, pendingApproval };
|