@jagreehal/workflow 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/index.cjs +9 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3266 -2
- package/dist/index.d.ts +3266 -2
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/docs/advanced.md +895 -0
- package/docs/api.md +257 -0
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,3266 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
1
|
+
import { WorkflowEvent, Result, AsyncResult, UnexpectedError, StepOptions, RetryOptions, TimeoutOptions, StepFailureMeta } from './core.cjs';
|
|
2
|
+
export { BackoffStrategy, CauseOf, EmptyInputError, ErrorOf, Errors, ExtractCause, ExtractError, ExtractValue, MaybeAsyncResult, PromiseRejectedError, PromiseRejectionCause, RunOptions, RunOptionsWithCatch, RunOptionsWithoutCatch, RunStep, STEP_TIMEOUT_MARKER, ScopeType, SettledError, StepTimeoutError, StepTimeoutMarkerMeta, UnexpectedCause, UnexpectedStepFailureCause, UnwrapError, all, allAsync, allSettled, allSettledAsync, andThen, any, anyAsync, err, from, fromNullable, fromPromise, getStepTimeoutMeta, isErr, isOk, isStepTimeoutError, isUnexpectedError, map, mapError, mapErrorTry, mapTry, match, ok, partition, run, tap, tapError, tryAsync, unwrap, unwrapOr, unwrapOrElse } from './core.cjs';
|
|
3
|
+
import { ResumeState, ResumeStateEntry, Workflow, WorkflowStrict, StepCache, AnyResultFn as AnyResultFn$1, ErrorsOfDeps as ErrorsOfDeps$1 } from './workflow.cjs';
|
|
4
|
+
export { ApprovalRejected, ApprovalStepOptions, CausesOfDeps, PendingApproval, WorkflowOptions, WorkflowOptionsStrict, clearStep, createApprovalStep, createHITLCollector, createStepCollector, createWorkflow, getPendingApprovals, hasPendingApproval, injectApproval, isApprovalRejected, isPendingApproval, isStepComplete, pendingApproval } from './workflow.cjs';
|
|
5
|
+
import { CollectableEvent, VisualizerOptions, DecisionStartEvent, DecisionBranchEvent, DecisionEndEvent, OutputFormat } from './visualize.cjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @jagreehal/workflow/conditional
|
|
9
|
+
*
|
|
10
|
+
* Conditional step execution helpers for workflows.
|
|
11
|
+
* These helpers allow you to conditionally execute steps based on runtime conditions,
|
|
12
|
+
* with proper event emission for skipped steps.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for conditional execution.
|
|
17
|
+
*/
|
|
18
|
+
type ConditionalOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Human-readable name for the conditional step.
|
|
21
|
+
* Used in step_skipped events for debugging and visualization.
|
|
22
|
+
*/
|
|
23
|
+
name?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Stable identity key for the conditional step.
|
|
26
|
+
* Used in step_skipped events for tracking and visualization.
|
|
27
|
+
*/
|
|
28
|
+
key?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional reason explaining why the step was skipped.
|
|
31
|
+
* Included in step_skipped events.
|
|
32
|
+
*/
|
|
33
|
+
reason?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Context for conditional execution, used to emit events.
|
|
37
|
+
*/
|
|
38
|
+
type ConditionalContext = {
|
|
39
|
+
/**
|
|
40
|
+
* The workflow ID for event emission.
|
|
41
|
+
*/
|
|
42
|
+
workflowId: string;
|
|
43
|
+
/**
|
|
44
|
+
* Event emitter function.
|
|
45
|
+
*/
|
|
46
|
+
onEvent?: (event: WorkflowEvent<unknown>) => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Type for operations that can be either sync or async.
|
|
50
|
+
*/
|
|
51
|
+
type MaybeAsync<T> = T | Promise<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Type for the operation function passed to conditional helpers.
|
|
54
|
+
*/
|
|
55
|
+
type Operation<T> = () => MaybeAsync<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Run a step only if condition is true, return undefined if skipped.
|
|
58
|
+
*
|
|
59
|
+
* Use this when you want to conditionally execute a step and handle
|
|
60
|
+
* the undefined case yourself. For a version with a default value,
|
|
61
|
+
* use `whenOr`.
|
|
62
|
+
*
|
|
63
|
+
* @param condition - Boolean condition to evaluate
|
|
64
|
+
* @param operation - Function that performs the step (only called if condition is true)
|
|
65
|
+
* @param options - Optional configuration for the conditional step
|
|
66
|
+
* @param ctx - Optional context for event emission
|
|
67
|
+
* @returns The result of the operation if condition is true, undefined otherwise
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const result = await workflow(async (step) => {
|
|
72
|
+
* const user = await step(fetchUser(id));
|
|
73
|
+
*
|
|
74
|
+
* // Only runs if user is premium
|
|
75
|
+
* const premium = await when(
|
|
76
|
+
* user.isPremium,
|
|
77
|
+
* () => step(() => fetchPremiumData(user.id), { name: 'premium-data' }),
|
|
78
|
+
* { name: 'check-premium', reason: 'User is not premium' }
|
|
79
|
+
* );
|
|
80
|
+
*
|
|
81
|
+
* return { user, premium };
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function when<T>(condition: boolean, operation: Operation<T>, options?: ConditionalOptions, ctx?: ConditionalContext): Promise<T | undefined>;
|
|
86
|
+
/**
|
|
87
|
+
* Synchronous overload for when the operation returns a non-Promise value.
|
|
88
|
+
*/
|
|
89
|
+
declare function when<T>(condition: boolean, operation: () => T, options?: ConditionalOptions, ctx?: ConditionalContext): T | undefined | Promise<T | undefined>;
|
|
90
|
+
/**
|
|
91
|
+
* Run a step only if condition is false, return undefined if skipped.
|
|
92
|
+
*
|
|
93
|
+
* Use this when you want to conditionally execute a step when a condition
|
|
94
|
+
* is NOT met. For a version with a default value, use `unlessOr`.
|
|
95
|
+
*
|
|
96
|
+
* @param condition - Boolean condition to evaluate
|
|
97
|
+
* @param operation - Function that performs the step (only called if condition is false)
|
|
98
|
+
* @param options - Optional configuration for the conditional step
|
|
99
|
+
* @param ctx - Optional context for event emission
|
|
100
|
+
* @returns The result of the operation if condition is false, undefined otherwise
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const result = await workflow(async (step) => {
|
|
105
|
+
* const user = await step(fetchUser(id));
|
|
106
|
+
*
|
|
107
|
+
* // Only runs if user is NOT verified
|
|
108
|
+
* const verification = await unless(
|
|
109
|
+
* user.isVerified,
|
|
110
|
+
* () => step(() => sendVerificationEmail(user.email), { name: 'send-verification' }),
|
|
111
|
+
* { name: 'check-verification', reason: 'User is already verified' }
|
|
112
|
+
* );
|
|
113
|
+
*
|
|
114
|
+
* return { user, verification };
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
declare function unless<T>(condition: boolean, operation: Operation<T>, options?: ConditionalOptions, ctx?: ConditionalContext): Promise<T | undefined>;
|
|
119
|
+
/**
|
|
120
|
+
* Synchronous overload for unless when the operation returns a non-Promise value.
|
|
121
|
+
*/
|
|
122
|
+
declare function unless<T>(condition: boolean, operation: () => T, options?: ConditionalOptions, ctx?: ConditionalContext): T | undefined | Promise<T | undefined>;
|
|
123
|
+
/**
|
|
124
|
+
* Run a step only if condition is true, return default value if skipped.
|
|
125
|
+
*
|
|
126
|
+
* Use this when you want to conditionally execute a step and provide
|
|
127
|
+
* a fallback value when the condition is not met.
|
|
128
|
+
*
|
|
129
|
+
* @param condition - Boolean condition to evaluate
|
|
130
|
+
* @param operation - Function that performs the step (only called if condition is true)
|
|
131
|
+
* @param defaultValue - Value to return if condition is false
|
|
132
|
+
* @param options - Optional configuration for the conditional step
|
|
133
|
+
* @param ctx - Optional context for event emission
|
|
134
|
+
* @returns The result of the operation if condition is true, defaultValue otherwise
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const result = await workflow(async (step) => {
|
|
139
|
+
* const user = await step(fetchUser(id));
|
|
140
|
+
*
|
|
141
|
+
* // Get premium limits or use default for non-premium users
|
|
142
|
+
* const limits = await whenOr(
|
|
143
|
+
* user.isPremium,
|
|
144
|
+
* () => step(() => fetchPremiumLimits(user.id), { name: 'premium-limits' }),
|
|
145
|
+
* { maxRequests: 100, maxStorage: 1000 }, // default for non-premium
|
|
146
|
+
* { name: 'check-premium-limits', reason: 'Using default limits for non-premium user' }
|
|
147
|
+
* );
|
|
148
|
+
*
|
|
149
|
+
* return { user, limits };
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
declare function whenOr<T, D>(condition: boolean, operation: Operation<T>, defaultValue: D, options?: ConditionalOptions, ctx?: ConditionalContext): Promise<T | D>;
|
|
154
|
+
/**
|
|
155
|
+
* Synchronous overload for whenOr when the operation returns a non-Promise value.
|
|
156
|
+
*/
|
|
157
|
+
declare function whenOr<T, D>(condition: boolean, operation: () => T, defaultValue: D, options?: ConditionalOptions, ctx?: ConditionalContext): T | D | Promise<T | D>;
|
|
158
|
+
/**
|
|
159
|
+
* Run a step only if condition is false, return default value if skipped.
|
|
160
|
+
*
|
|
161
|
+
* Use this when you want to conditionally execute a step when a condition
|
|
162
|
+
* is NOT met, with a fallback value for when the condition is true.
|
|
163
|
+
*
|
|
164
|
+
* @param condition - Boolean condition to evaluate
|
|
165
|
+
* @param operation - Function that performs the step (only called if condition is false)
|
|
166
|
+
* @param defaultValue - Value to return if condition is true
|
|
167
|
+
* @param options - Optional configuration for the conditional step
|
|
168
|
+
* @param ctx - Optional context for event emission
|
|
169
|
+
* @returns The result of the operation if condition is false, defaultValue otherwise
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const result = await workflow(async (step) => {
|
|
174
|
+
* const user = await step(fetchUser(id));
|
|
175
|
+
*
|
|
176
|
+
* // Generate new token if user is NOT authenticated, otherwise use existing
|
|
177
|
+
* const token = await unlessOr(
|
|
178
|
+
* user.isAuthenticated,
|
|
179
|
+
* () => step(() => generateNewToken(user.id), { name: 'generate-token' }),
|
|
180
|
+
* user.existingToken, // use existing token if authenticated
|
|
181
|
+
* { name: 'check-auth-for-token', reason: 'Using existing token for authenticated user' }
|
|
182
|
+
* );
|
|
183
|
+
*
|
|
184
|
+
* return { user, token };
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
declare function unlessOr<T, D>(condition: boolean, operation: Operation<T>, defaultValue: D, options?: ConditionalOptions, ctx?: ConditionalContext): Promise<T | D>;
|
|
189
|
+
/**
|
|
190
|
+
* Synchronous overload for unlessOr when the operation returns a non-Promise value.
|
|
191
|
+
*/
|
|
192
|
+
declare function unlessOr<T, D>(condition: boolean, operation: () => T, defaultValue: D, options?: ConditionalOptions, ctx?: ConditionalContext): T | D | Promise<T | D>;
|
|
193
|
+
/**
|
|
194
|
+
* Create a set of conditional helpers bound to a workflow context.
|
|
195
|
+
*
|
|
196
|
+
* Use this factory when you want to automatically emit step_skipped events
|
|
197
|
+
* to the workflow's event stream without passing context manually.
|
|
198
|
+
*
|
|
199
|
+
* @param ctx - The workflow context containing workflowId and onEvent
|
|
200
|
+
* @returns Object with bound when, unless, whenOr, and unlessOr functions
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const result = await run(async (step) => {
|
|
205
|
+
* const ctx = { workflowId, onEvent };
|
|
206
|
+
* const { when, whenOr } = createConditionalHelpers(ctx);
|
|
207
|
+
*
|
|
208
|
+
* const user = await step(fetchUser(id));
|
|
209
|
+
*
|
|
210
|
+
* const premium = await when(
|
|
211
|
+
* user.isPremium,
|
|
212
|
+
* () => step(() => fetchPremiumData(user.id)),
|
|
213
|
+
* { name: 'premium-data' }
|
|
214
|
+
* );
|
|
215
|
+
*
|
|
216
|
+
* return { user, premium };
|
|
217
|
+
* }, { onEvent, workflowId });
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
declare function createConditionalHelpers(ctx: ConditionalContext): {
|
|
221
|
+
/**
|
|
222
|
+
* Run a step only if condition is true, return undefined if skipped.
|
|
223
|
+
*/
|
|
224
|
+
when: <T>(condition: boolean, operation: Operation<T>, options?: ConditionalOptions) => MaybeAsync<T | undefined>;
|
|
225
|
+
/**
|
|
226
|
+
* Run a step only if condition is false, return undefined if skipped.
|
|
227
|
+
*/
|
|
228
|
+
unless: <T>(condition: boolean, operation: Operation<T>, options?: ConditionalOptions) => MaybeAsync<T | undefined>;
|
|
229
|
+
/**
|
|
230
|
+
* Run a step only if condition is true, return default value if skipped.
|
|
231
|
+
*/
|
|
232
|
+
whenOr: <T, D>(condition: boolean, operation: Operation<T>, defaultValue: D, options?: ConditionalOptions) => MaybeAsync<T | D>;
|
|
233
|
+
/**
|
|
234
|
+
* Run a step only if condition is false, return default value if skipped.
|
|
235
|
+
*/
|
|
236
|
+
unlessOr: <T, D>(condition: boolean, operation: Operation<T>, defaultValue: D, options?: ConditionalOptions) => MaybeAsync<T | D>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Circuit Breaker for Steps
|
|
241
|
+
*
|
|
242
|
+
* Prevents cascading failures by tracking step failure rates and
|
|
243
|
+
* short-circuiting calls when a threshold is exceeded.
|
|
244
|
+
*
|
|
245
|
+
* Uses the circuit breaker pattern with three states:
|
|
246
|
+
* - CLOSED: Normal operation (steps executing)
|
|
247
|
+
* - OPEN: Fast-fail mode (steps blocked)
|
|
248
|
+
* - HALF_OPEN: Testing if service recovered
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* import { createCircuitBreaker } from '@jagreehal/workflow';
|
|
253
|
+
*
|
|
254
|
+
* const breaker = createCircuitBreaker({
|
|
255
|
+
* failureThreshold: 5,
|
|
256
|
+
* resetTimeout: 30000,
|
|
257
|
+
* halfOpenMax: 3,
|
|
258
|
+
* });
|
|
259
|
+
*
|
|
260
|
+
* const result = await workflow(async (step) => {
|
|
261
|
+
* const data = await breaker.execute(
|
|
262
|
+
* () => step(() => callExternalApi()),
|
|
263
|
+
* { name: 'external-api' }
|
|
264
|
+
* );
|
|
265
|
+
* return data;
|
|
266
|
+
* });
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Circuit breaker state.
|
|
272
|
+
*/
|
|
273
|
+
type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
|
|
274
|
+
/**
|
|
275
|
+
* Configuration for circuit breaker behavior.
|
|
276
|
+
*/
|
|
277
|
+
interface CircuitBreakerConfig {
|
|
278
|
+
/**
|
|
279
|
+
* Number of failures within the window before opening the circuit.
|
|
280
|
+
* @default 5
|
|
281
|
+
*/
|
|
282
|
+
failureThreshold: number;
|
|
283
|
+
/**
|
|
284
|
+
* Time in ms to wait before transitioning from OPEN to HALF_OPEN.
|
|
285
|
+
* @default 30000 (30 seconds)
|
|
286
|
+
*/
|
|
287
|
+
resetTimeout: number;
|
|
288
|
+
/**
|
|
289
|
+
* Time window in ms for counting failures.
|
|
290
|
+
* Failures older than this are discarded.
|
|
291
|
+
* @default 60000 (1 minute)
|
|
292
|
+
*/
|
|
293
|
+
windowSize: number;
|
|
294
|
+
/**
|
|
295
|
+
* Maximum number of test requests allowed in HALF_OPEN state.
|
|
296
|
+
* If all succeed, circuit closes. If any fail, circuit reopens.
|
|
297
|
+
* @default 3
|
|
298
|
+
*/
|
|
299
|
+
halfOpenMax: number;
|
|
300
|
+
/**
|
|
301
|
+
* Optional callback when circuit state changes.
|
|
302
|
+
*/
|
|
303
|
+
onStateChange?: (from: CircuitState, to: CircuitState, name?: string) => void;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Error thrown when the circuit is open and calls are blocked.
|
|
307
|
+
*/
|
|
308
|
+
declare class CircuitOpenError extends Error {
|
|
309
|
+
readonly type: "CIRCUIT_OPEN";
|
|
310
|
+
readonly circuitName: string;
|
|
311
|
+
readonly state: CircuitState;
|
|
312
|
+
readonly retryAfterMs: number;
|
|
313
|
+
constructor(options: {
|
|
314
|
+
circuitName: string;
|
|
315
|
+
state: CircuitState;
|
|
316
|
+
retryAfterMs: number;
|
|
317
|
+
message?: string;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Type guard for CircuitOpenError.
|
|
322
|
+
*/
|
|
323
|
+
declare function isCircuitOpenError(error: unknown): error is CircuitOpenError;
|
|
324
|
+
/**
|
|
325
|
+
* Circuit breaker statistics.
|
|
326
|
+
*/
|
|
327
|
+
interface CircuitBreakerStats {
|
|
328
|
+
state: CircuitState;
|
|
329
|
+
failureCount: number;
|
|
330
|
+
successCount: number;
|
|
331
|
+
lastFailureTime: number | null;
|
|
332
|
+
lastSuccessTime: number | null;
|
|
333
|
+
halfOpenSuccesses: number;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Circuit breaker instance for protecting external calls.
|
|
337
|
+
*/
|
|
338
|
+
interface CircuitBreaker {
|
|
339
|
+
/**
|
|
340
|
+
* Execute an operation with circuit breaker protection.
|
|
341
|
+
* Throws CircuitOpenError if the circuit is open.
|
|
342
|
+
*
|
|
343
|
+
* @param operation - The operation to execute
|
|
344
|
+
* @param options - Optional name for logging/metrics
|
|
345
|
+
* @returns The operation result
|
|
346
|
+
* @throws CircuitOpenError if circuit is open
|
|
347
|
+
*/
|
|
348
|
+
execute<T>(operation: () => T | Promise<T>, options?: {
|
|
349
|
+
name?: string;
|
|
350
|
+
}): Promise<T>;
|
|
351
|
+
/**
|
|
352
|
+
* Execute a Result-returning operation with circuit breaker protection.
|
|
353
|
+
* Returns a CircuitOpenError result instead of throwing.
|
|
354
|
+
*
|
|
355
|
+
* @param operation - The operation returning a Result
|
|
356
|
+
* @param options - Optional name for logging/metrics
|
|
357
|
+
* @returns Result with the value or CircuitOpenError
|
|
358
|
+
*/
|
|
359
|
+
executeResult<T, E>(operation: () => Result<T, E> | AsyncResult<T, E>, options?: {
|
|
360
|
+
name?: string;
|
|
361
|
+
}): AsyncResult<T, E | CircuitOpenError>;
|
|
362
|
+
/**
|
|
363
|
+
* Get current circuit state.
|
|
364
|
+
*/
|
|
365
|
+
getState(): CircuitState;
|
|
366
|
+
/**
|
|
367
|
+
* Get circuit breaker statistics.
|
|
368
|
+
*/
|
|
369
|
+
getStats(): CircuitBreakerStats;
|
|
370
|
+
/**
|
|
371
|
+
* Manually reset the circuit breaker to CLOSED state.
|
|
372
|
+
*/
|
|
373
|
+
reset(): void;
|
|
374
|
+
/**
|
|
375
|
+
* Manually open the circuit (for testing or manual intervention).
|
|
376
|
+
*/
|
|
377
|
+
forceOpen(): void;
|
|
378
|
+
/**
|
|
379
|
+
* Record a manual success (useful for health checks).
|
|
380
|
+
*/
|
|
381
|
+
recordSuccess(): void;
|
|
382
|
+
/**
|
|
383
|
+
* Record a manual failure (useful for health checks).
|
|
384
|
+
*/
|
|
385
|
+
recordFailure(error?: unknown): void;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Create a circuit breaker instance.
|
|
389
|
+
*
|
|
390
|
+
* @param name - Name for this circuit breaker (used in errors and logging)
|
|
391
|
+
* @param config - Configuration options
|
|
392
|
+
* @returns A CircuitBreaker instance
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* const apiBreaker = createCircuitBreaker('external-api', {
|
|
397
|
+
* failureThreshold: 5,
|
|
398
|
+
* resetTimeout: 30000,
|
|
399
|
+
* });
|
|
400
|
+
*
|
|
401
|
+
* // In workflow
|
|
402
|
+
* const data = await apiBreaker.execute(() =>
|
|
403
|
+
* step(() => fetchFromApi(id))
|
|
404
|
+
* );
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
declare function createCircuitBreaker(name: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker;
|
|
408
|
+
/**
|
|
409
|
+
* Preset configurations for common use cases.
|
|
410
|
+
*/
|
|
411
|
+
declare const circuitBreakerPresets: {
|
|
412
|
+
/**
|
|
413
|
+
* Aggressive circuit breaker for critical paths.
|
|
414
|
+
* Opens quickly (3 failures) and recovers slowly (60s).
|
|
415
|
+
*/
|
|
416
|
+
readonly critical: {
|
|
417
|
+
failureThreshold: number;
|
|
418
|
+
resetTimeout: number;
|
|
419
|
+
windowSize: number;
|
|
420
|
+
halfOpenMax: number;
|
|
421
|
+
};
|
|
422
|
+
/**
|
|
423
|
+
* Standard circuit breaker for typical API calls.
|
|
424
|
+
* Balanced between stability and availability.
|
|
425
|
+
*/
|
|
426
|
+
readonly standard: {
|
|
427
|
+
failureThreshold: number;
|
|
428
|
+
resetTimeout: number;
|
|
429
|
+
windowSize: number;
|
|
430
|
+
halfOpenMax: number;
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Lenient circuit breaker for non-critical operations.
|
|
434
|
+
* Opens slowly (10 failures) and recovers quickly (15s).
|
|
435
|
+
*/
|
|
436
|
+
readonly lenient: {
|
|
437
|
+
failureThreshold: number;
|
|
438
|
+
resetTimeout: number;
|
|
439
|
+
windowSize: number;
|
|
440
|
+
halfOpenMax: number;
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Saga / Compensation Pattern
|
|
446
|
+
*
|
|
447
|
+
* Define compensating actions for steps that need rollback on downstream failures.
|
|
448
|
+
* When a workflow fails after some steps have completed, compensations run in
|
|
449
|
+
* reverse order automatically.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* import { createSagaWorkflow, ok, err } from '@jagreehal/workflow';
|
|
454
|
+
*
|
|
455
|
+
* const checkout = createSagaWorkflow({ reserveInventory, chargeCard, sendEmail });
|
|
456
|
+
*
|
|
457
|
+
* const result = await checkout(async (saga) => {
|
|
458
|
+
* const reservation = await saga.step(
|
|
459
|
+
* () => reserveInventory(items),
|
|
460
|
+
* { compensate: (res) => releaseInventory(res.reservationId) }
|
|
461
|
+
* );
|
|
462
|
+
*
|
|
463
|
+
* const payment = await saga.step(
|
|
464
|
+
* () => chargeCard(amount),
|
|
465
|
+
* { compensate: (p) => refundPayment(p.txId) }
|
|
466
|
+
* );
|
|
467
|
+
*
|
|
468
|
+
* await saga.step(() => sendEmail(userId)); // No compensation needed
|
|
469
|
+
*
|
|
470
|
+
* return { reservation, payment };
|
|
471
|
+
* });
|
|
472
|
+
* // On failure: compensations run in reverse order automatically
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* A compensation action to run on rollback.
|
|
478
|
+
*/
|
|
479
|
+
type CompensationAction<T> = (value: T) => void | Promise<void>;
|
|
480
|
+
/**
|
|
481
|
+
* Options for a saga step.
|
|
482
|
+
*/
|
|
483
|
+
interface SagaStepOptions<T> {
|
|
484
|
+
/**
|
|
485
|
+
* Name for the step (used in logging and events).
|
|
486
|
+
*/
|
|
487
|
+
name?: string;
|
|
488
|
+
/**
|
|
489
|
+
* Compensation action to run if a later step fails.
|
|
490
|
+
* Receives the value returned by this step.
|
|
491
|
+
*/
|
|
492
|
+
compensate?: CompensationAction<T>;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Error returned when compensation actions fail.
|
|
496
|
+
*/
|
|
497
|
+
interface SagaCompensationError {
|
|
498
|
+
type: "SAGA_COMPENSATION_ERROR";
|
|
499
|
+
/** The original error that triggered the saga rollback */
|
|
500
|
+
originalError: unknown;
|
|
501
|
+
/** Errors from failed compensation actions */
|
|
502
|
+
compensationErrors: Array<{
|
|
503
|
+
stepName?: string;
|
|
504
|
+
error: unknown;
|
|
505
|
+
}>;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Type guard for SagaCompensationError.
|
|
509
|
+
*/
|
|
510
|
+
declare function isSagaCompensationError(error: unknown): error is SagaCompensationError;
|
|
511
|
+
/**
|
|
512
|
+
* Saga execution context provided to the workflow callback.
|
|
513
|
+
*/
|
|
514
|
+
interface SagaContext<E = unknown> {
|
|
515
|
+
/**
|
|
516
|
+
* Execute a step with optional compensation.
|
|
517
|
+
*
|
|
518
|
+
* @param operation - The operation to execute (returns Result)
|
|
519
|
+
* @param options - Step options including compensation action
|
|
520
|
+
* @returns The unwrapped success value
|
|
521
|
+
*/
|
|
522
|
+
step: <T, StepE extends E, StepC = unknown>(operation: () => Result<T, StepE, StepC> | AsyncResult<T, StepE, StepC>, options?: SagaStepOptions<T>) => Promise<T>;
|
|
523
|
+
/**
|
|
524
|
+
* Execute a throwing operation with optional compensation.
|
|
525
|
+
*
|
|
526
|
+
* @param operation - The operation to execute (may throw)
|
|
527
|
+
* @param options - Step options including error mapping and compensation
|
|
528
|
+
* @returns The success value
|
|
529
|
+
*/
|
|
530
|
+
tryStep: <T, Err extends E>(operation: () => T | Promise<T>, options: {
|
|
531
|
+
error: Err;
|
|
532
|
+
name?: string;
|
|
533
|
+
compensate?: CompensationAction<T>;
|
|
534
|
+
} | {
|
|
535
|
+
onError: (cause: unknown) => Err;
|
|
536
|
+
name?: string;
|
|
537
|
+
compensate?: CompensationAction<T>;
|
|
538
|
+
}) => Promise<T>;
|
|
539
|
+
/**
|
|
540
|
+
* Get all recorded compensations (for debugging/testing).
|
|
541
|
+
*/
|
|
542
|
+
getCompensations: () => Array<{
|
|
543
|
+
name?: string;
|
|
544
|
+
hasValue: boolean;
|
|
545
|
+
}>;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Saga event types for observability.
|
|
549
|
+
*/
|
|
550
|
+
type SagaEvent = {
|
|
551
|
+
type: "saga_start";
|
|
552
|
+
sagaId: string;
|
|
553
|
+
ts: number;
|
|
554
|
+
} | {
|
|
555
|
+
type: "saga_success";
|
|
556
|
+
sagaId: string;
|
|
557
|
+
ts: number;
|
|
558
|
+
durationMs: number;
|
|
559
|
+
} | {
|
|
560
|
+
type: "saga_error";
|
|
561
|
+
sagaId: string;
|
|
562
|
+
ts: number;
|
|
563
|
+
durationMs: number;
|
|
564
|
+
error: unknown;
|
|
565
|
+
} | {
|
|
566
|
+
type: "saga_compensation_start";
|
|
567
|
+
sagaId: string;
|
|
568
|
+
ts: number;
|
|
569
|
+
stepCount: number;
|
|
570
|
+
} | {
|
|
571
|
+
type: "saga_compensation_step";
|
|
572
|
+
sagaId: string;
|
|
573
|
+
stepName?: string;
|
|
574
|
+
ts: number;
|
|
575
|
+
success: boolean;
|
|
576
|
+
error?: unknown;
|
|
577
|
+
} | {
|
|
578
|
+
type: "saga_compensation_end";
|
|
579
|
+
sagaId: string;
|
|
580
|
+
ts: number;
|
|
581
|
+
durationMs: number;
|
|
582
|
+
success: boolean;
|
|
583
|
+
failedCount: number;
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* Options for createSagaWorkflow.
|
|
587
|
+
*/
|
|
588
|
+
interface SagaWorkflowOptions<E> {
|
|
589
|
+
/**
|
|
590
|
+
* Called when errors occur.
|
|
591
|
+
*/
|
|
592
|
+
onError?: (error: E | UnexpectedError | SagaCompensationError, stepName?: string) => void;
|
|
593
|
+
/**
|
|
594
|
+
* Event stream for saga lifecycle events.
|
|
595
|
+
*/
|
|
596
|
+
onEvent?: (event: SagaEvent | WorkflowEvent<E | UnexpectedError>) => void;
|
|
597
|
+
/**
|
|
598
|
+
* Whether to throw if compensation actions fail.
|
|
599
|
+
* If false, original error is returned with compensation errors attached.
|
|
600
|
+
* @default false
|
|
601
|
+
*/
|
|
602
|
+
throwOnCompensationFailure?: boolean;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Result type for saga workflow.
|
|
606
|
+
*/
|
|
607
|
+
type SagaResult<T, E> = Result<T, E | UnexpectedError | SagaCompensationError, unknown>;
|
|
608
|
+
/**
|
|
609
|
+
* Helper type for Result-returning functions.
|
|
610
|
+
*/
|
|
611
|
+
type AnyResultFn = (...args: any[]) => Result<any, any, any> | Promise<Result<any, any, any>>;
|
|
612
|
+
/**
|
|
613
|
+
* Extract union of error types from a deps object.
|
|
614
|
+
*/
|
|
615
|
+
type ErrorsOfDeps<Deps extends Record<string, AnyResultFn>> = {
|
|
616
|
+
[K in keyof Deps]: Deps[K] extends (...args: never[]) => infer R ? R extends Promise<infer PR> ? PR extends {
|
|
617
|
+
ok: false;
|
|
618
|
+
error: infer E;
|
|
619
|
+
} ? E : never : R extends {
|
|
620
|
+
ok: false;
|
|
621
|
+
error: infer E;
|
|
622
|
+
} ? E : never : never;
|
|
623
|
+
}[keyof Deps];
|
|
624
|
+
/**
|
|
625
|
+
* Create a saga workflow with automatic compensation on failure.
|
|
626
|
+
*
|
|
627
|
+
* @param deps - Object mapping names to Result-returning functions
|
|
628
|
+
* @param options - Saga workflow options
|
|
629
|
+
* @returns A saga executor function
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```typescript
|
|
633
|
+
* const saga = createSagaWorkflow({ reserveInventory, chargeCard });
|
|
634
|
+
*
|
|
635
|
+
* const result = await saga(async (ctx) => {
|
|
636
|
+
* const reservation = await ctx.step(
|
|
637
|
+
* () => reserveInventory(items),
|
|
638
|
+
* { compensate: (res) => releaseInventory(res.id) }
|
|
639
|
+
* );
|
|
640
|
+
*
|
|
641
|
+
* const payment = await ctx.step(
|
|
642
|
+
* () => chargeCard(amount),
|
|
643
|
+
* { compensate: (p) => refundPayment(p.txId) }
|
|
644
|
+
* );
|
|
645
|
+
*
|
|
646
|
+
* return { reservation, payment };
|
|
647
|
+
* });
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
declare function createSagaWorkflow<const Deps extends Readonly<Record<string, AnyResultFn>>>(deps: Deps, options?: SagaWorkflowOptions<ErrorsOfDeps<Deps>>): <T>(fn: (saga: SagaContext<ErrorsOfDeps<Deps>>, deps: Deps) => Promise<T>) => Promise<SagaResult<T, ErrorsOfDeps<Deps>>>;
|
|
651
|
+
/**
|
|
652
|
+
* Run a saga with explicit compensation registration.
|
|
653
|
+
*
|
|
654
|
+
* Lower-level API for when you don't want automatic error inference
|
|
655
|
+
* from a deps object.
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* ```typescript
|
|
659
|
+
* const result = await runSaga<CheckoutResult, CheckoutError>(async (saga) => {
|
|
660
|
+
* const reservation = await saga.step(
|
|
661
|
+
* () => reserveInventory(items),
|
|
662
|
+
* { compensate: (res) => releaseInventory(res.id) }
|
|
663
|
+
* );
|
|
664
|
+
* return { reservation };
|
|
665
|
+
* });
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
668
|
+
declare function runSaga<T, E>(fn: (saga: SagaContext<E>) => Promise<T>, options?: Omit<SagaWorkflowOptions<E>, "onEvent"> & {
|
|
669
|
+
onEvent?: (event: SagaEvent) => void;
|
|
670
|
+
}): Promise<SagaResult<T, E>>;
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Rate Limiting / Concurrency Control
|
|
674
|
+
*
|
|
675
|
+
* Control throughput for steps that hit rate-limited APIs or shared resources.
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* ```typescript
|
|
679
|
+
* import { createRateLimiter, createConcurrencyLimiter } from '@jagreehal/workflow';
|
|
680
|
+
*
|
|
681
|
+
* // Rate limiting (requests per second)
|
|
682
|
+
* const rateLimiter = createRateLimiter({ maxPerSecond: 10 });
|
|
683
|
+
*
|
|
684
|
+
* // Concurrency limiting (max concurrent)
|
|
685
|
+
* const concurrencyLimiter = createConcurrencyLimiter({ maxConcurrent: 5 });
|
|
686
|
+
*
|
|
687
|
+
* const result = await workflow(async (step) => {
|
|
688
|
+
* // Wrap operations with rate limiting
|
|
689
|
+
* const data = await rateLimiter.execute(() =>
|
|
690
|
+
* step(() => callRateLimitedApi())
|
|
691
|
+
* );
|
|
692
|
+
*
|
|
693
|
+
* // Wrap batch operations with concurrency control
|
|
694
|
+
* const results = await concurrencyLimiter.executeAll(
|
|
695
|
+
* ids.map(id => () => step(() => fetchItem(id)))
|
|
696
|
+
* );
|
|
697
|
+
*
|
|
698
|
+
* return { data, results };
|
|
699
|
+
* });
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Configuration for rate limiter.
|
|
705
|
+
*/
|
|
706
|
+
interface RateLimiterConfig {
|
|
707
|
+
/**
|
|
708
|
+
* Maximum operations per second.
|
|
709
|
+
*/
|
|
710
|
+
maxPerSecond: number;
|
|
711
|
+
/**
|
|
712
|
+
* Burst capacity - allows brief spikes above the rate.
|
|
713
|
+
* @default maxPerSecond * 2
|
|
714
|
+
*/
|
|
715
|
+
burstCapacity?: number;
|
|
716
|
+
/**
|
|
717
|
+
* Strategy when rate limit is exceeded.
|
|
718
|
+
* - 'wait': Wait until a slot is available (default)
|
|
719
|
+
* - 'reject': Reject immediately with error
|
|
720
|
+
* @default 'wait'
|
|
721
|
+
*/
|
|
722
|
+
strategy?: "wait" | "reject";
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Configuration for concurrency limiter.
|
|
726
|
+
*/
|
|
727
|
+
interface ConcurrencyLimiterConfig {
|
|
728
|
+
/**
|
|
729
|
+
* Maximum concurrent operations.
|
|
730
|
+
*/
|
|
731
|
+
maxConcurrent: number;
|
|
732
|
+
/**
|
|
733
|
+
* Strategy when limit is reached.
|
|
734
|
+
* - 'queue': Queue and wait (default)
|
|
735
|
+
* - 'reject': Reject immediately
|
|
736
|
+
* @default 'queue'
|
|
737
|
+
*/
|
|
738
|
+
strategy?: "queue" | "reject";
|
|
739
|
+
/**
|
|
740
|
+
* Maximum queue size (only for 'queue' strategy).
|
|
741
|
+
* @default Infinity
|
|
742
|
+
*/
|
|
743
|
+
maxQueueSize?: number;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Error when rate/concurrency limit is exceeded and strategy is 'reject'.
|
|
747
|
+
*/
|
|
748
|
+
interface RateLimitExceededError {
|
|
749
|
+
type: "RATE_LIMIT_EXCEEDED";
|
|
750
|
+
limiterName: string;
|
|
751
|
+
retryAfterMs?: number;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Error when concurrency limit queue is full.
|
|
755
|
+
*/
|
|
756
|
+
interface QueueFullError {
|
|
757
|
+
type: "QUEUE_FULL";
|
|
758
|
+
limiterName: string;
|
|
759
|
+
queueSize: number;
|
|
760
|
+
maxQueueSize: number;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Type guard for RateLimitExceededError.
|
|
764
|
+
*/
|
|
765
|
+
declare function isRateLimitExceededError(error: unknown): error is RateLimitExceededError;
|
|
766
|
+
/**
|
|
767
|
+
* Type guard for QueueFullError.
|
|
768
|
+
*/
|
|
769
|
+
declare function isQueueFullError(error: unknown): error is QueueFullError;
|
|
770
|
+
/**
|
|
771
|
+
* Statistics for rate limiter.
|
|
772
|
+
*/
|
|
773
|
+
interface RateLimiterStats {
|
|
774
|
+
availableTokens: number;
|
|
775
|
+
maxTokens: number;
|
|
776
|
+
tokensPerSecond: number;
|
|
777
|
+
waitingCount: number;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Statistics for concurrency limiter.
|
|
781
|
+
*/
|
|
782
|
+
interface ConcurrencyLimiterStats {
|
|
783
|
+
activeCount: number;
|
|
784
|
+
maxConcurrent: number;
|
|
785
|
+
queueSize: number;
|
|
786
|
+
maxQueueSize: number;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Rate limiter interface.
|
|
790
|
+
*/
|
|
791
|
+
interface RateLimiter {
|
|
792
|
+
/**
|
|
793
|
+
* Execute an operation with rate limiting.
|
|
794
|
+
* @param operation - The operation to execute
|
|
795
|
+
* @returns The operation result
|
|
796
|
+
*/
|
|
797
|
+
execute<T>(operation: () => T | Promise<T>): Promise<T>;
|
|
798
|
+
/**
|
|
799
|
+
* Execute a Result-returning operation with rate limiting.
|
|
800
|
+
*/
|
|
801
|
+
executeResult<T, E>(operation: () => Result<T, E> | AsyncResult<T, E>): AsyncResult<T, E | RateLimitExceededError>;
|
|
802
|
+
/**
|
|
803
|
+
* Get current statistics.
|
|
804
|
+
*/
|
|
805
|
+
getStats(): RateLimiterStats;
|
|
806
|
+
/**
|
|
807
|
+
* Reset the rate limiter.
|
|
808
|
+
*/
|
|
809
|
+
reset(): void;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Create a token bucket rate limiter.
|
|
813
|
+
*
|
|
814
|
+
* @param name - Name for the limiter (used in errors)
|
|
815
|
+
* @param config - Rate limiter configuration
|
|
816
|
+
* @returns A RateLimiter instance
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```typescript
|
|
820
|
+
* const limiter = createRateLimiter('api-calls', {
|
|
821
|
+
* maxPerSecond: 10,
|
|
822
|
+
* burstCapacity: 20,
|
|
823
|
+
* });
|
|
824
|
+
*
|
|
825
|
+
* // In workflow
|
|
826
|
+
* const data = await limiter.execute(() =>
|
|
827
|
+
* step(() => callApi())
|
|
828
|
+
* );
|
|
829
|
+
* ```
|
|
830
|
+
*/
|
|
831
|
+
declare function createRateLimiter(name: string, config: RateLimiterConfig): RateLimiter;
|
|
832
|
+
/**
|
|
833
|
+
* Concurrency limiter interface.
|
|
834
|
+
*/
|
|
835
|
+
interface ConcurrencyLimiter {
|
|
836
|
+
/**
|
|
837
|
+
* Execute an operation with concurrency limiting.
|
|
838
|
+
* @param operation - The operation to execute
|
|
839
|
+
* @returns The operation result
|
|
840
|
+
*/
|
|
841
|
+
execute<T>(operation: () => T | Promise<T>): Promise<T>;
|
|
842
|
+
/**
|
|
843
|
+
* Execute multiple operations with concurrency control.
|
|
844
|
+
* @param operations - Array of operation factories
|
|
845
|
+
* @returns Array of results (in order)
|
|
846
|
+
*/
|
|
847
|
+
executeAll<T>(operations: Array<() => T | Promise<T>>): Promise<T[]>;
|
|
848
|
+
/**
|
|
849
|
+
* Execute a Result-returning operation with concurrency limiting.
|
|
850
|
+
*/
|
|
851
|
+
executeResult<T, E>(operation: () => Result<T, E> | AsyncResult<T, E>): AsyncResult<T, E | QueueFullError>;
|
|
852
|
+
/**
|
|
853
|
+
* Get current statistics.
|
|
854
|
+
*/
|
|
855
|
+
getStats(): ConcurrencyLimiterStats;
|
|
856
|
+
/**
|
|
857
|
+
* Reset the concurrency limiter.
|
|
858
|
+
*/
|
|
859
|
+
reset(): void;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Create a concurrency limiter.
|
|
863
|
+
*
|
|
864
|
+
* @param name - Name for the limiter (used in errors)
|
|
865
|
+
* @param config - Concurrency limiter configuration
|
|
866
|
+
* @returns A ConcurrencyLimiter instance
|
|
867
|
+
*
|
|
868
|
+
* @example
|
|
869
|
+
* ```typescript
|
|
870
|
+
* const limiter = createConcurrencyLimiter('db-pool', {
|
|
871
|
+
* maxConcurrent: 10,
|
|
872
|
+
* });
|
|
873
|
+
*
|
|
874
|
+
* // Execute with concurrency control
|
|
875
|
+
* const results = await limiter.executeAll(
|
|
876
|
+
* ids.map(id => () => fetchItem(id))
|
|
877
|
+
* );
|
|
878
|
+
* ```
|
|
879
|
+
*/
|
|
880
|
+
declare function createConcurrencyLimiter(name: string, config: ConcurrencyLimiterConfig): ConcurrencyLimiter;
|
|
881
|
+
/**
|
|
882
|
+
* Configuration for combined rate + concurrency limiter.
|
|
883
|
+
*/
|
|
884
|
+
interface CombinedLimiterConfig {
|
|
885
|
+
/**
|
|
886
|
+
* Rate limiting configuration.
|
|
887
|
+
*/
|
|
888
|
+
rate?: RateLimiterConfig;
|
|
889
|
+
/**
|
|
890
|
+
* Concurrency limiting configuration.
|
|
891
|
+
*/
|
|
892
|
+
concurrency?: ConcurrencyLimiterConfig;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Create a combined rate + concurrency limiter.
|
|
896
|
+
*
|
|
897
|
+
* Operations are first rate-limited, then concurrency-limited.
|
|
898
|
+
*
|
|
899
|
+
* @param name - Name for the limiter
|
|
900
|
+
* @param config - Combined limiter configuration
|
|
901
|
+
* @returns An object with both limiters and a combined execute function
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* ```typescript
|
|
905
|
+
* const limiter = createCombinedLimiter('api', {
|
|
906
|
+
* rate: { maxPerSecond: 10 },
|
|
907
|
+
* concurrency: { maxConcurrent: 5 },
|
|
908
|
+
* });
|
|
909
|
+
*
|
|
910
|
+
* const result = await limiter.execute(() => callApi());
|
|
911
|
+
* ```
|
|
912
|
+
*/
|
|
913
|
+
declare function createCombinedLimiter(name: string, config: CombinedLimiterConfig): {
|
|
914
|
+
rate?: RateLimiter;
|
|
915
|
+
concurrency?: ConcurrencyLimiter;
|
|
916
|
+
execute: <T>(operation: () => T | Promise<T>) => Promise<T>;
|
|
917
|
+
};
|
|
918
|
+
/**
|
|
919
|
+
* Preset configurations for common use cases.
|
|
920
|
+
*/
|
|
921
|
+
declare const rateLimiterPresets: {
|
|
922
|
+
/**
|
|
923
|
+
* Typical API rate limit (10 req/s).
|
|
924
|
+
*/
|
|
925
|
+
readonly api: {
|
|
926
|
+
maxPerSecond: number;
|
|
927
|
+
burstCapacity: number;
|
|
928
|
+
strategy: "wait";
|
|
929
|
+
};
|
|
930
|
+
/**
|
|
931
|
+
* Database pool limit (concurrent connections).
|
|
932
|
+
*/
|
|
933
|
+
readonly database: {
|
|
934
|
+
maxConcurrent: number;
|
|
935
|
+
strategy: "queue";
|
|
936
|
+
maxQueueSize: number;
|
|
937
|
+
};
|
|
938
|
+
/**
|
|
939
|
+
* Aggressive rate limit for external APIs (5 req/s).
|
|
940
|
+
*/
|
|
941
|
+
readonly external: {
|
|
942
|
+
maxPerSecond: number;
|
|
943
|
+
burstCapacity: number;
|
|
944
|
+
strategy: "wait";
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Workflow Versioning and Migration
|
|
950
|
+
*
|
|
951
|
+
* Handle schema changes when resuming workflows that were persisted
|
|
952
|
+
* with older step shapes.
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* ```typescript
|
|
956
|
+
* import { createVersionedWorkflow } from '@jagreehal/workflow';
|
|
957
|
+
*
|
|
958
|
+
* const workflow = createVersionedWorkflow(
|
|
959
|
+
* { fetchUser, chargeCard },
|
|
960
|
+
* {
|
|
961
|
+
* version: 2,
|
|
962
|
+
* migrations: {
|
|
963
|
+
* 1: (state) => migrateV1ToV2(state),
|
|
964
|
+
* },
|
|
965
|
+
* resumeState: loadState(runId),
|
|
966
|
+
* }
|
|
967
|
+
* );
|
|
968
|
+
* ```
|
|
969
|
+
*/
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Version number type.
|
|
973
|
+
*/
|
|
974
|
+
type Version = number;
|
|
975
|
+
/**
|
|
976
|
+
* Migration function that transforms state from one version to the next.
|
|
977
|
+
*/
|
|
978
|
+
type MigrationFn = (state: ResumeState) => ResumeState | Promise<ResumeState>;
|
|
979
|
+
/**
|
|
980
|
+
* Map of migrations keyed by the source version.
|
|
981
|
+
* Migration at key N transforms state from version N to version N+1.
|
|
982
|
+
*/
|
|
983
|
+
type Migrations = Record<Version, MigrationFn>;
|
|
984
|
+
/**
|
|
985
|
+
* Versioned state includes the version number.
|
|
986
|
+
*/
|
|
987
|
+
interface VersionedState {
|
|
988
|
+
version: Version;
|
|
989
|
+
state: ResumeState;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Configuration for versioned workflow.
|
|
993
|
+
*/
|
|
994
|
+
interface VersionedWorkflowConfig {
|
|
995
|
+
/**
|
|
996
|
+
* Current workflow version.
|
|
997
|
+
*/
|
|
998
|
+
version: Version;
|
|
999
|
+
/**
|
|
1000
|
+
* Migrations for upgrading old states.
|
|
1001
|
+
* Key is the source version, value transforms to next version.
|
|
1002
|
+
*/
|
|
1003
|
+
migrations?: Migrations;
|
|
1004
|
+
/**
|
|
1005
|
+
* Strict mode - fail if state version is higher than current.
|
|
1006
|
+
* @default true
|
|
1007
|
+
*/
|
|
1008
|
+
strictVersioning?: boolean;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Error when version migration fails.
|
|
1012
|
+
*/
|
|
1013
|
+
interface MigrationError {
|
|
1014
|
+
type: "MIGRATION_ERROR";
|
|
1015
|
+
fromVersion: Version;
|
|
1016
|
+
toVersion: Version;
|
|
1017
|
+
cause: unknown;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Error when state version is incompatible.
|
|
1021
|
+
*/
|
|
1022
|
+
interface VersionIncompatibleError {
|
|
1023
|
+
type: "VERSION_INCOMPATIBLE";
|
|
1024
|
+
stateVersion: Version;
|
|
1025
|
+
currentVersion: Version;
|
|
1026
|
+
reason: string;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Type guard for MigrationError.
|
|
1030
|
+
*/
|
|
1031
|
+
declare function isMigrationError(error: unknown): error is MigrationError;
|
|
1032
|
+
/**
|
|
1033
|
+
* Type guard for VersionIncompatibleError.
|
|
1034
|
+
*/
|
|
1035
|
+
declare function isVersionIncompatibleError(error: unknown): error is VersionIncompatibleError;
|
|
1036
|
+
/**
|
|
1037
|
+
* Migrate state from one version to another.
|
|
1038
|
+
*
|
|
1039
|
+
* @param state - The versioned state to migrate
|
|
1040
|
+
* @param targetVersion - The target version
|
|
1041
|
+
* @param migrations - Migration functions
|
|
1042
|
+
* @returns The migrated state or an error
|
|
1043
|
+
*
|
|
1044
|
+
* @example
|
|
1045
|
+
* ```typescript
|
|
1046
|
+
* const migrated = await migrateState(
|
|
1047
|
+
* { version: 1, state: oldState },
|
|
1048
|
+
* 3,
|
|
1049
|
+
* {
|
|
1050
|
+
* 1: (s) => transformV1ToV2(s),
|
|
1051
|
+
* 2: (s) => transformV2ToV3(s),
|
|
1052
|
+
* }
|
|
1053
|
+
* );
|
|
1054
|
+
* ```
|
|
1055
|
+
*/
|
|
1056
|
+
declare function migrateState(state: VersionedState, targetVersion: Version, migrations: Migrations): Promise<Result<VersionedState, MigrationError | VersionIncompatibleError>>;
|
|
1057
|
+
/**
|
|
1058
|
+
* Create a versioned resume state loader.
|
|
1059
|
+
*
|
|
1060
|
+
* This wraps a state loader to automatically apply migrations
|
|
1061
|
+
* when loading older state versions.
|
|
1062
|
+
*
|
|
1063
|
+
* @param config - Versioning configuration
|
|
1064
|
+
* @returns A function that loads and migrates state
|
|
1065
|
+
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* ```typescript
|
|
1068
|
+
* const loadVersionedState = createVersionedStateLoader({
|
|
1069
|
+
* version: 3,
|
|
1070
|
+
* migrations: {
|
|
1071
|
+
* 1: migrateV1ToV2,
|
|
1072
|
+
* 2: migrateV2ToV3,
|
|
1073
|
+
* },
|
|
1074
|
+
* });
|
|
1075
|
+
*
|
|
1076
|
+
* // In workflow
|
|
1077
|
+
* const workflow = createWorkflow(deps, {
|
|
1078
|
+
* resumeState: () => loadVersionedState(savedState),
|
|
1079
|
+
* });
|
|
1080
|
+
* ```
|
|
1081
|
+
*/
|
|
1082
|
+
declare function createVersionedStateLoader(config: VersionedWorkflowConfig): (versionedState: VersionedState | null | undefined) => Promise<Result<ResumeState | undefined, MigrationError | VersionIncompatibleError>>;
|
|
1083
|
+
/**
|
|
1084
|
+
* Create versioned state from current resume state.
|
|
1085
|
+
*
|
|
1086
|
+
* Use this when saving state to storage.
|
|
1087
|
+
*
|
|
1088
|
+
* @param state - The current resume state
|
|
1089
|
+
* @param version - The current workflow version
|
|
1090
|
+
* @returns A versioned state object
|
|
1091
|
+
*
|
|
1092
|
+
* @example
|
|
1093
|
+
* ```typescript
|
|
1094
|
+
* const collector = createStepCollector();
|
|
1095
|
+
* // ... run workflow ...
|
|
1096
|
+
*
|
|
1097
|
+
* const versionedState = createVersionedState(collector.getState(), 2);
|
|
1098
|
+
* await db.saveWorkflowState(workflowId, versionedState);
|
|
1099
|
+
* ```
|
|
1100
|
+
*/
|
|
1101
|
+
declare function createVersionedState(state: ResumeState, version: Version): VersionedState;
|
|
1102
|
+
/**
|
|
1103
|
+
* Parse versioned state from JSON.
|
|
1104
|
+
*
|
|
1105
|
+
* Handles the serialization/deserialization of ResumeState with Map.
|
|
1106
|
+
*
|
|
1107
|
+
* @param json - The JSON string or parsed object
|
|
1108
|
+
* @returns The versioned state or null if invalid
|
|
1109
|
+
*
|
|
1110
|
+
* @example
|
|
1111
|
+
* ```typescript
|
|
1112
|
+
* const json = await db.loadWorkflowState(workflowId);
|
|
1113
|
+
* const versionedState = parseVersionedState(json);
|
|
1114
|
+
* if (versionedState) {
|
|
1115
|
+
* const loader = createVersionedStateLoader(config);
|
|
1116
|
+
* const state = await loader(versionedState);
|
|
1117
|
+
* }
|
|
1118
|
+
* ```
|
|
1119
|
+
*/
|
|
1120
|
+
interface SerializedVersionedState {
|
|
1121
|
+
version: number;
|
|
1122
|
+
state: {
|
|
1123
|
+
steps: Array<[string, ResumeStateEntry]>;
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
declare function parseVersionedState(json: string | SerializedVersionedState | null | undefined): VersionedState | null;
|
|
1127
|
+
/**
|
|
1128
|
+
* Serialize versioned state to JSON.
|
|
1129
|
+
*
|
|
1130
|
+
* Converts the Map to an array for JSON serialization.
|
|
1131
|
+
*
|
|
1132
|
+
* @param state - The versioned state
|
|
1133
|
+
* @returns JSON string
|
|
1134
|
+
*
|
|
1135
|
+
* @example
|
|
1136
|
+
* ```typescript
|
|
1137
|
+
* const json = stringifyVersionedState(versionedState);
|
|
1138
|
+
* await db.saveWorkflowState(workflowId, json);
|
|
1139
|
+
* ```
|
|
1140
|
+
*/
|
|
1141
|
+
declare function stringifyVersionedState(state: VersionedState): string;
|
|
1142
|
+
/**
|
|
1143
|
+
* Create a migration that renames step keys.
|
|
1144
|
+
*
|
|
1145
|
+
* @param renames - Map of old key to new key
|
|
1146
|
+
* @returns A migration function
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* ```typescript
|
|
1150
|
+
* const migrations = {
|
|
1151
|
+
* 1: createKeyRenameMigration({
|
|
1152
|
+
* 'user:fetch': 'user:load',
|
|
1153
|
+
* 'order:create': 'order:submit',
|
|
1154
|
+
* }),
|
|
1155
|
+
* };
|
|
1156
|
+
* ```
|
|
1157
|
+
*/
|
|
1158
|
+
declare function createKeyRenameMigration(renames: Record<string, string>): MigrationFn;
|
|
1159
|
+
/**
|
|
1160
|
+
* Create a migration that removes specific step keys.
|
|
1161
|
+
*
|
|
1162
|
+
* @param keysToRemove - Array of keys to remove
|
|
1163
|
+
* @returns A migration function
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* ```typescript
|
|
1167
|
+
* const migrations = {
|
|
1168
|
+
* 1: createKeyRemoveMigration(['deprecated:step', 'old:cache']),
|
|
1169
|
+
* };
|
|
1170
|
+
* ```
|
|
1171
|
+
*/
|
|
1172
|
+
declare function createKeyRemoveMigration(keysToRemove: string[]): MigrationFn;
|
|
1173
|
+
/**
|
|
1174
|
+
* Create a migration that transforms step values.
|
|
1175
|
+
*
|
|
1176
|
+
* @param transforms - Map of key to transform function
|
|
1177
|
+
* @returns A migration function
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```typescript
|
|
1181
|
+
* const migrations = {
|
|
1182
|
+
* 1: createValueTransformMigration({
|
|
1183
|
+
* 'user:fetch': (entry) => ({
|
|
1184
|
+
* ...entry,
|
|
1185
|
+
* result: entry.result.ok
|
|
1186
|
+
* ? ok({ ...entry.result.value, newField: 'default' })
|
|
1187
|
+
* : entry.result,
|
|
1188
|
+
* }),
|
|
1189
|
+
* }),
|
|
1190
|
+
* };
|
|
1191
|
+
* ```
|
|
1192
|
+
*/
|
|
1193
|
+
declare function createValueTransformMigration(transforms: Record<string, (entry: ResumeStateEntry) => ResumeStateEntry>): MigrationFn;
|
|
1194
|
+
/**
|
|
1195
|
+
* Compose multiple migrations into a single migration.
|
|
1196
|
+
*
|
|
1197
|
+
* @param migrations - Array of migration functions
|
|
1198
|
+
* @returns A single migration function that applies all migrations in order
|
|
1199
|
+
*
|
|
1200
|
+
* @example
|
|
1201
|
+
* ```typescript
|
|
1202
|
+
* const migrations = {
|
|
1203
|
+
* 1: composeMigrations([
|
|
1204
|
+
* createKeyRenameMigration({ 'old': 'new' }),
|
|
1205
|
+
* createKeyRemoveMigration(['deprecated']),
|
|
1206
|
+
* ]),
|
|
1207
|
+
* };
|
|
1208
|
+
* ```
|
|
1209
|
+
*/
|
|
1210
|
+
declare function composeMigrations(migrations: MigrationFn[]): MigrationFn;
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Autotel Integration
|
|
1214
|
+
*
|
|
1215
|
+
* First-class OpenTelemetry spans and metrics from the workflow event stream
|
|
1216
|
+
* using the autotel library.
|
|
1217
|
+
*
|
|
1218
|
+
* @example
|
|
1219
|
+
* ```typescript
|
|
1220
|
+
* import { createAutotelAdapter } from '@jagreehal/workflow/autotel';
|
|
1221
|
+
* import { init } from 'autotel';
|
|
1222
|
+
*
|
|
1223
|
+
* // Initialize autotel
|
|
1224
|
+
* init({ service: 'checkout-api' });
|
|
1225
|
+
*
|
|
1226
|
+
* // Create adapter
|
|
1227
|
+
* const otel = createAutotelAdapter({ serviceName: 'checkout' });
|
|
1228
|
+
*
|
|
1229
|
+
* // Use with workflow
|
|
1230
|
+
* const workflow = createWorkflow(deps, { onEvent: otel.handleEvent });
|
|
1231
|
+
*
|
|
1232
|
+
* // Automatic spans: workflow > step > retry attempts
|
|
1233
|
+
* // Automatic metrics: step_duration, retry_count, error_rate
|
|
1234
|
+
* ```
|
|
1235
|
+
*/
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Configuration for the autotel adapter.
|
|
1239
|
+
*/
|
|
1240
|
+
interface AutotelAdapterConfig {
|
|
1241
|
+
/**
|
|
1242
|
+
* Service name for spans (used as prefix).
|
|
1243
|
+
*/
|
|
1244
|
+
serviceName?: string;
|
|
1245
|
+
/**
|
|
1246
|
+
* Whether to create spans for each step.
|
|
1247
|
+
* @default true
|
|
1248
|
+
*/
|
|
1249
|
+
createStepSpans?: boolean;
|
|
1250
|
+
/**
|
|
1251
|
+
* Whether to record metrics for steps.
|
|
1252
|
+
* @default true
|
|
1253
|
+
*/
|
|
1254
|
+
recordMetrics?: boolean;
|
|
1255
|
+
/**
|
|
1256
|
+
* Custom attributes to add to all spans.
|
|
1257
|
+
*/
|
|
1258
|
+
defaultAttributes?: Record<string, string | number | boolean>;
|
|
1259
|
+
/**
|
|
1260
|
+
* Whether to record retry events as span events.
|
|
1261
|
+
* @default true
|
|
1262
|
+
*/
|
|
1263
|
+
recordRetryEvents?: boolean;
|
|
1264
|
+
/**
|
|
1265
|
+
* Whether to mark workflow errors as span errors.
|
|
1266
|
+
* @default true
|
|
1267
|
+
*/
|
|
1268
|
+
markErrorsOnSpan?: boolean;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Metrics collected by the adapter.
|
|
1272
|
+
*/
|
|
1273
|
+
interface AutotelMetrics {
|
|
1274
|
+
stepDurations: Array<{
|
|
1275
|
+
name: string;
|
|
1276
|
+
durationMs: number;
|
|
1277
|
+
success: boolean;
|
|
1278
|
+
attributes?: Record<string, string | number | boolean>;
|
|
1279
|
+
}>;
|
|
1280
|
+
retryCount: number;
|
|
1281
|
+
errorCount: number;
|
|
1282
|
+
cacheHits: number;
|
|
1283
|
+
cacheMisses: number;
|
|
1284
|
+
/** Default attributes applied to all metrics */
|
|
1285
|
+
defaultAttributes: Record<string, string | number | boolean>;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Autotel adapter interface.
|
|
1289
|
+
*/
|
|
1290
|
+
interface AutotelAdapter {
|
|
1291
|
+
/**
|
|
1292
|
+
* Handle workflow events (pass to onEvent option).
|
|
1293
|
+
*/
|
|
1294
|
+
handleEvent: (event: WorkflowEvent<unknown>) => void;
|
|
1295
|
+
/**
|
|
1296
|
+
* Get current active spans count (for debugging).
|
|
1297
|
+
*/
|
|
1298
|
+
getActiveSpansCount: () => {
|
|
1299
|
+
workflows: number;
|
|
1300
|
+
steps: number;
|
|
1301
|
+
};
|
|
1302
|
+
/**
|
|
1303
|
+
* Get collected metrics.
|
|
1304
|
+
*/
|
|
1305
|
+
getMetrics: () => AutotelMetrics;
|
|
1306
|
+
/**
|
|
1307
|
+
* Reset adapter state.
|
|
1308
|
+
*/
|
|
1309
|
+
reset: () => void;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Create an autotel adapter for workflow observability.
|
|
1313
|
+
*
|
|
1314
|
+
* This adapter translates workflow events into metrics and tracking data.
|
|
1315
|
+
* When autotel is installed and initialized, it will also create OpenTelemetry spans.
|
|
1316
|
+
*
|
|
1317
|
+
* @param config - Adapter configuration
|
|
1318
|
+
* @returns An AutotelAdapter instance
|
|
1319
|
+
*
|
|
1320
|
+
* @example
|
|
1321
|
+
* ```typescript
|
|
1322
|
+
* import { createAutotelAdapter } from '@jagreehal/workflow/autotel';
|
|
1323
|
+
*
|
|
1324
|
+
* const otel = createAutotelAdapter({ serviceName: 'checkout' });
|
|
1325
|
+
*
|
|
1326
|
+
* const workflow = createWorkflow(deps, {
|
|
1327
|
+
* onEvent: otel.handleEvent,
|
|
1328
|
+
* });
|
|
1329
|
+
* ```
|
|
1330
|
+
*/
|
|
1331
|
+
declare function createAutotelAdapter(config?: AutotelAdapterConfig): AutotelAdapter;
|
|
1332
|
+
/**
|
|
1333
|
+
* Create an event handler that integrates with autotel's trace() function.
|
|
1334
|
+
*
|
|
1335
|
+
* This is a higher-level integration that creates actual OpenTelemetry spans
|
|
1336
|
+
* for each workflow and step when autotel is initialized.
|
|
1337
|
+
*
|
|
1338
|
+
* @param options - Configuration options
|
|
1339
|
+
* @returns An event handler function
|
|
1340
|
+
*
|
|
1341
|
+
* @example
|
|
1342
|
+
* ```typescript
|
|
1343
|
+
* import { createAutotelEventHandler } from '@jagreehal/workflow/autotel';
|
|
1344
|
+
* import { init } from 'autotel';
|
|
1345
|
+
*
|
|
1346
|
+
* init({ service: 'checkout-api' });
|
|
1347
|
+
*
|
|
1348
|
+
* const handler = createAutotelEventHandler({ serviceName: 'checkout' });
|
|
1349
|
+
*
|
|
1350
|
+
* const workflow = createWorkflow(deps, {
|
|
1351
|
+
* onEvent: handler,
|
|
1352
|
+
* });
|
|
1353
|
+
* ```
|
|
1354
|
+
*/
|
|
1355
|
+
declare function createAutotelEventHandler(options?: {
|
|
1356
|
+
serviceName?: string;
|
|
1357
|
+
includeStepDetails?: boolean;
|
|
1358
|
+
}): (event: WorkflowEvent<unknown>) => void;
|
|
1359
|
+
/**
|
|
1360
|
+
* Wrapper type for autotel trace function.
|
|
1361
|
+
* Use this when you want to explicitly type the autotel integration.
|
|
1362
|
+
*/
|
|
1363
|
+
type AutotelTraceFn = <T>(name: string, fn: (ctx: {
|
|
1364
|
+
setAttribute: (key: string, value: unknown) => void;
|
|
1365
|
+
}) => T | Promise<T>) => T | Promise<T>;
|
|
1366
|
+
/**
|
|
1367
|
+
* Create a workflow wrapper that adds autotel tracing.
|
|
1368
|
+
*
|
|
1369
|
+
* This creates a higher-order function that wraps workflow execution
|
|
1370
|
+
* in an autotel trace span.
|
|
1371
|
+
*
|
|
1372
|
+
* @param traceFn - The autotel trace function
|
|
1373
|
+
* @param options - Wrapper options
|
|
1374
|
+
* @returns A workflow wrapper function
|
|
1375
|
+
*
|
|
1376
|
+
* @example
|
|
1377
|
+
* ```typescript
|
|
1378
|
+
* import { trace } from 'autotel';
|
|
1379
|
+
* import { withAutotelTracing } from '@jagreehal/workflow/autotel';
|
|
1380
|
+
*
|
|
1381
|
+
* const traced = withAutotelTracing(trace, { serviceName: 'checkout' });
|
|
1382
|
+
*
|
|
1383
|
+
* const result = await traced('process-order', async () => {
|
|
1384
|
+
* return workflow(async (step) => {
|
|
1385
|
+
* // ... workflow logic
|
|
1386
|
+
* });
|
|
1387
|
+
* });
|
|
1388
|
+
* ```
|
|
1389
|
+
*/
|
|
1390
|
+
declare function withAutotelTracing(traceFn: AutotelTraceFn, options?: {
|
|
1391
|
+
serviceName?: string;
|
|
1392
|
+
}): <T>(name: string, fn: () => T | Promise<T>, attributes?: Record<string, string | number | boolean>) => Promise<T>;
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* @jagreehal/workflow/webhook
|
|
1396
|
+
*
|
|
1397
|
+
* Webhook and event trigger adapters for exposing workflows as HTTP endpoints.
|
|
1398
|
+
* Framework-agnostic handlers that work with Express, Hono, Fastify, etc.
|
|
1399
|
+
*/
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Generic HTTP request representation.
|
|
1403
|
+
* Abstracts away framework-specific request objects.
|
|
1404
|
+
*/
|
|
1405
|
+
interface WebhookRequest<Body = unknown> {
|
|
1406
|
+
/** HTTP method (GET, POST, PUT, DELETE, etc.) */
|
|
1407
|
+
method: string;
|
|
1408
|
+
/** Request path (e.g., "/api/checkout") */
|
|
1409
|
+
path: string;
|
|
1410
|
+
/** Request headers */
|
|
1411
|
+
headers: Record<string, string | string[] | undefined>;
|
|
1412
|
+
/** Parsed request body (JSON) */
|
|
1413
|
+
body: Body;
|
|
1414
|
+
/** Query parameters */
|
|
1415
|
+
query: Record<string, string | string[] | undefined>;
|
|
1416
|
+
/** Path parameters (e.g., { id: "123" }) */
|
|
1417
|
+
params: Record<string, string>;
|
|
1418
|
+
/** Raw request object from framework (for advanced use cases) */
|
|
1419
|
+
raw?: unknown;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Generic HTTP response representation.
|
|
1423
|
+
*/
|
|
1424
|
+
interface WebhookResponse<T = unknown> {
|
|
1425
|
+
/** HTTP status code */
|
|
1426
|
+
status: number;
|
|
1427
|
+
/** Response headers */
|
|
1428
|
+
headers?: Record<string, string>;
|
|
1429
|
+
/** Response body (will be JSON serialized) */
|
|
1430
|
+
body: T;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Error response body structure.
|
|
1434
|
+
*/
|
|
1435
|
+
interface ErrorResponseBody {
|
|
1436
|
+
error: {
|
|
1437
|
+
type: string;
|
|
1438
|
+
message?: string;
|
|
1439
|
+
details?: unknown;
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Input validation result.
|
|
1444
|
+
*/
|
|
1445
|
+
type ValidationResult<T, E = string> = Result<T, E>;
|
|
1446
|
+
/**
|
|
1447
|
+
* Standard validation error type.
|
|
1448
|
+
*/
|
|
1449
|
+
interface ValidationError {
|
|
1450
|
+
type: "VALIDATION_ERROR";
|
|
1451
|
+
message: string;
|
|
1452
|
+
field?: string;
|
|
1453
|
+
details?: unknown;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Type guard for ValidationError.
|
|
1457
|
+
*/
|
|
1458
|
+
declare function isValidationError(e: unknown): e is ValidationError;
|
|
1459
|
+
/**
|
|
1460
|
+
* Configuration for creating a webhook handler.
|
|
1461
|
+
*
|
|
1462
|
+
* @template TInput - The validated input type
|
|
1463
|
+
* @template TOutput - The workflow output type
|
|
1464
|
+
* @template TError - The workflow error type
|
|
1465
|
+
* @template TBody - The raw request body type
|
|
1466
|
+
*/
|
|
1467
|
+
interface WebhookHandlerConfig<TInput, TOutput, TError, TBody = unknown> {
|
|
1468
|
+
/**
|
|
1469
|
+
* Validate and transform the incoming request.
|
|
1470
|
+
* Return ok(input) to proceed, or err(validationError) to reject.
|
|
1471
|
+
*
|
|
1472
|
+
* @param req - The incoming request
|
|
1473
|
+
* @returns Validated input or validation error
|
|
1474
|
+
*/
|
|
1475
|
+
validateInput: (req: WebhookRequest<TBody>) => ValidationResult<TInput, ValidationError> | Promise<ValidationResult<TInput, ValidationError>>;
|
|
1476
|
+
/**
|
|
1477
|
+
* Map workflow result to HTTP response.
|
|
1478
|
+
* Called for both success and error cases.
|
|
1479
|
+
*
|
|
1480
|
+
* @param result - The workflow result
|
|
1481
|
+
* @param req - The original request (for context)
|
|
1482
|
+
* @returns HTTP response
|
|
1483
|
+
*/
|
|
1484
|
+
mapResult: (result: Result<TOutput, TError | UnexpectedError>, req: WebhookRequest<TBody>) => WebhookResponse;
|
|
1485
|
+
/**
|
|
1486
|
+
* Optional: Map validation errors to HTTP response.
|
|
1487
|
+
* Defaults to 400 Bad Request with error details.
|
|
1488
|
+
*
|
|
1489
|
+
* @param error - The validation error
|
|
1490
|
+
* @param req - The original request
|
|
1491
|
+
* @returns HTTP response
|
|
1492
|
+
*/
|
|
1493
|
+
mapValidationError?: (error: ValidationError, req: WebhookRequest<TBody>) => WebhookResponse<ErrorResponseBody>;
|
|
1494
|
+
/**
|
|
1495
|
+
* Optional: Handle unexpected errors during request processing.
|
|
1496
|
+
* Defaults to 500 Internal Server Error.
|
|
1497
|
+
*
|
|
1498
|
+
* @param error - The unexpected error
|
|
1499
|
+
* @param req - The original request
|
|
1500
|
+
* @returns HTTP response
|
|
1501
|
+
*/
|
|
1502
|
+
mapUnexpectedError?: (error: unknown, req: WebhookRequest<TBody>) => WebhookResponse<ErrorResponseBody>;
|
|
1503
|
+
/**
|
|
1504
|
+
* Optional: Request middleware.
|
|
1505
|
+
* Transform or enrich the request before validation.
|
|
1506
|
+
*
|
|
1507
|
+
* @param req - The incoming request
|
|
1508
|
+
* @returns Transformed request
|
|
1509
|
+
*/
|
|
1510
|
+
beforeValidation?: (req: WebhookRequest<TBody>) => WebhookRequest<TBody> | Promise<WebhookRequest<TBody>>;
|
|
1511
|
+
/**
|
|
1512
|
+
* Optional: Response middleware.
|
|
1513
|
+
* Transform the response before sending.
|
|
1514
|
+
*
|
|
1515
|
+
* @param response - The response to send
|
|
1516
|
+
* @param req - The original request
|
|
1517
|
+
* @returns Transformed response
|
|
1518
|
+
*/
|
|
1519
|
+
afterResponse?: (response: WebhookResponse, req: WebhookRequest<TBody>) => WebhookResponse | Promise<WebhookResponse>;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* A webhook handler function that processes requests.
|
|
1523
|
+
*/
|
|
1524
|
+
type WebhookHandler<TBody = unknown> = (req: WebhookRequest<TBody>) => Promise<WebhookResponse>;
|
|
1525
|
+
/**
|
|
1526
|
+
* Default validation error mapper.
|
|
1527
|
+
* Returns 400 Bad Request with error details.
|
|
1528
|
+
*/
|
|
1529
|
+
declare function defaultValidationErrorMapper(error: ValidationError): WebhookResponse<ErrorResponseBody>;
|
|
1530
|
+
/**
|
|
1531
|
+
* Default unexpected error mapper.
|
|
1532
|
+
* Returns 500 Internal Server Error.
|
|
1533
|
+
*/
|
|
1534
|
+
declare function defaultUnexpectedErrorMapper(_error: unknown): WebhookResponse<ErrorResponseBody>;
|
|
1535
|
+
/**
|
|
1536
|
+
* Create a webhook handler for a workflow.
|
|
1537
|
+
*
|
|
1538
|
+
* This factory creates an HTTP handler function that:
|
|
1539
|
+
* 1. Validates the incoming request
|
|
1540
|
+
* 2. Executes the workflow with the validated input
|
|
1541
|
+
* 3. Maps the result to an HTTP response
|
|
1542
|
+
*
|
|
1543
|
+
* The handler is framework-agnostic and returns a standard response object.
|
|
1544
|
+
* Use framework adapters (createExpressHandler, createHonoHandler, etc.) to
|
|
1545
|
+
* integrate with specific frameworks.
|
|
1546
|
+
*
|
|
1547
|
+
* @template TInput - The validated input type passed to the workflow
|
|
1548
|
+
* @template TOutput - The workflow output type
|
|
1549
|
+
* @template TError - The workflow error type
|
|
1550
|
+
* @template TBody - The raw request body type
|
|
1551
|
+
* @template TDeps - The workflow dependencies type
|
|
1552
|
+
*
|
|
1553
|
+
* @param workflow - The workflow function to execute
|
|
1554
|
+
* @param workflowFn - The workflow body function (step, deps, input) => output
|
|
1555
|
+
* @param config - Handler configuration
|
|
1556
|
+
* @returns A webhook handler function
|
|
1557
|
+
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```typescript
|
|
1560
|
+
* const checkoutWorkflow = createWorkflow({ chargeCard, sendEmail });
|
|
1561
|
+
*
|
|
1562
|
+
* const handler = createWebhookHandler(
|
|
1563
|
+
* checkoutWorkflow,
|
|
1564
|
+
* async (step, deps, input: CheckoutInput) => {
|
|
1565
|
+
* const charge = await step(() => deps.chargeCard(input.amount));
|
|
1566
|
+
* await step(() => deps.sendEmail(input.email, charge.receiptUrl));
|
|
1567
|
+
* return { chargeId: charge.id };
|
|
1568
|
+
* },
|
|
1569
|
+
* {
|
|
1570
|
+
* validateInput: (req) => {
|
|
1571
|
+
* const { amount, email } = req.body;
|
|
1572
|
+
* if (!amount || !email) {
|
|
1573
|
+
* return err({ type: 'VALIDATION_ERROR', message: 'Missing required fields' });
|
|
1574
|
+
* }
|
|
1575
|
+
* return ok({ amount, email });
|
|
1576
|
+
* },
|
|
1577
|
+
* mapResult: (result) => {
|
|
1578
|
+
* if (result.ok) {
|
|
1579
|
+
* return { status: 200, body: result.value };
|
|
1580
|
+
* }
|
|
1581
|
+
* if (result.error === 'CARD_DECLINED') {
|
|
1582
|
+
* return { status: 402, body: { error: { type: 'CARD_DECLINED' } } };
|
|
1583
|
+
* }
|
|
1584
|
+
* return { status: 500, body: { error: { type: 'UNKNOWN' } } };
|
|
1585
|
+
* },
|
|
1586
|
+
* }
|
|
1587
|
+
* );
|
|
1588
|
+
*
|
|
1589
|
+
* // Use with Express
|
|
1590
|
+
* app.post('/checkout', async (req, res) => {
|
|
1591
|
+
* const response = await handler(toWebhookRequest(req));
|
|
1592
|
+
* res.status(response.status).json(response.body);
|
|
1593
|
+
* });
|
|
1594
|
+
* ```
|
|
1595
|
+
*/
|
|
1596
|
+
declare function createWebhookHandler<TInput, TOutput, TError, TBody = unknown, TDeps = unknown>(workflow: Workflow<TError, TDeps> | WorkflowStrict<TError, unknown, TDeps>, workflowFn: (step: Parameters<Parameters<Workflow<TError, TDeps>>[1]>[0], deps: TDeps, input: TInput) => TOutput | Promise<TOutput>, config: WebhookHandlerConfig<TInput, TOutput, TError, TBody>): WebhookHandler<TBody>;
|
|
1597
|
+
/**
|
|
1598
|
+
* Configuration for a simple webhook handler without workflow.
|
|
1599
|
+
*/
|
|
1600
|
+
interface SimpleHandlerConfig<TInput, TOutput, TError, TBody = unknown> {
|
|
1601
|
+
validateInput: (req: WebhookRequest<TBody>) => ValidationResult<TInput, ValidationError> | Promise<ValidationResult<TInput, ValidationError>>;
|
|
1602
|
+
handler: (input: TInput, req: WebhookRequest<TBody>) => AsyncResult<TOutput, TError>;
|
|
1603
|
+
mapResult: (result: Result<TOutput, TError>, req: WebhookRequest<TBody>) => WebhookResponse;
|
|
1604
|
+
mapValidationError?: (error: ValidationError, req: WebhookRequest<TBody>) => WebhookResponse<ErrorResponseBody>;
|
|
1605
|
+
mapUnexpectedError?: (error: unknown, req: WebhookRequest<TBody>) => WebhookResponse<ErrorResponseBody>;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Create a simple webhook handler without workflow orchestration.
|
|
1609
|
+
* Useful for simple endpoints that don't need step-based error handling.
|
|
1610
|
+
*
|
|
1611
|
+
* @example
|
|
1612
|
+
* ```typescript
|
|
1613
|
+
* const handler = createSimpleHandler({
|
|
1614
|
+
* validateInput: (req) => {
|
|
1615
|
+
* const { id } = req.params;
|
|
1616
|
+
* if (!id) return err({ type: 'VALIDATION_ERROR', message: 'Missing id' });
|
|
1617
|
+
* return ok({ id });
|
|
1618
|
+
* },
|
|
1619
|
+
* handler: async ({ id }) => {
|
|
1620
|
+
* const user = await db.findUser(id);
|
|
1621
|
+
* return user ? ok(user) : err('NOT_FOUND' as const);
|
|
1622
|
+
* },
|
|
1623
|
+
* mapResult: (result) => {
|
|
1624
|
+
* if (result.ok) return { status: 200, body: result.value };
|
|
1625
|
+
* return { status: 404, body: { error: { type: 'NOT_FOUND' } } };
|
|
1626
|
+
* },
|
|
1627
|
+
* });
|
|
1628
|
+
* ```
|
|
1629
|
+
*/
|
|
1630
|
+
declare function createSimpleHandler<TInput, TOutput, TError, TBody = unknown>(config: SimpleHandlerConfig<TInput, TOutput, TError, TBody>): WebhookHandler<TBody>;
|
|
1631
|
+
/**
|
|
1632
|
+
* Standard error mapping configuration.
|
|
1633
|
+
*/
|
|
1634
|
+
interface ErrorMapping<TError> {
|
|
1635
|
+
/** The error value to match */
|
|
1636
|
+
error: TError;
|
|
1637
|
+
/** HTTP status code for this error */
|
|
1638
|
+
status: number;
|
|
1639
|
+
/** Optional custom message */
|
|
1640
|
+
message?: string;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Create a result mapper from error mappings.
|
|
1644
|
+
* Provides a declarative way to map workflow errors to HTTP responses.
|
|
1645
|
+
*
|
|
1646
|
+
* @param mappings - Array of error mappings
|
|
1647
|
+
* @param defaultStatus - Default status for unmapped errors (default: 500)
|
|
1648
|
+
* @returns A mapResult function for use in handler config
|
|
1649
|
+
*
|
|
1650
|
+
* @example
|
|
1651
|
+
* ```typescript
|
|
1652
|
+
* const mapResult = createResultMapper<CheckoutOutput, CheckoutError>([
|
|
1653
|
+
* { error: 'NOT_FOUND', status: 404, message: 'Resource not found' },
|
|
1654
|
+
* { error: 'CARD_DECLINED', status: 402, message: 'Payment failed' },
|
|
1655
|
+
* { error: 'RATE_LIMITED', status: 429, message: 'Too many requests' },
|
|
1656
|
+
* ]);
|
|
1657
|
+
*
|
|
1658
|
+
* const handler = createWebhookHandler(workflow, workflowFn, {
|
|
1659
|
+
* validateInput,
|
|
1660
|
+
* mapResult,
|
|
1661
|
+
* });
|
|
1662
|
+
* ```
|
|
1663
|
+
*/
|
|
1664
|
+
declare function createResultMapper<TOutput, TError>(mappings: ErrorMapping<TError>[], options?: {
|
|
1665
|
+
defaultStatus?: number;
|
|
1666
|
+
successStatus?: number;
|
|
1667
|
+
}): (result: Result<TOutput, TError | UnexpectedError>) => WebhookResponse;
|
|
1668
|
+
/**
|
|
1669
|
+
* Express-style request object (minimal interface).
|
|
1670
|
+
*/
|
|
1671
|
+
interface ExpressLikeRequest {
|
|
1672
|
+
method: string;
|
|
1673
|
+
path: string;
|
|
1674
|
+
headers: Record<string, string | string[] | undefined>;
|
|
1675
|
+
body: unknown;
|
|
1676
|
+
query: Record<string, string | string[] | undefined>;
|
|
1677
|
+
params: Record<string, string>;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Express-style response object (minimal interface).
|
|
1681
|
+
*/
|
|
1682
|
+
interface ExpressLikeResponse {
|
|
1683
|
+
status(code: number): ExpressLikeResponse;
|
|
1684
|
+
set(headers: Record<string, string>): ExpressLikeResponse;
|
|
1685
|
+
json(body: unknown): void;
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Convert an Express-like request to WebhookRequest.
|
|
1689
|
+
*
|
|
1690
|
+
* @param req - Express-like request object
|
|
1691
|
+
* @returns WebhookRequest
|
|
1692
|
+
*
|
|
1693
|
+
* @example
|
|
1694
|
+
* ```typescript
|
|
1695
|
+
* app.post('/checkout', async (req, res) => {
|
|
1696
|
+
* const webhookReq = toWebhookRequest(req);
|
|
1697
|
+
* const response = await handler(webhookReq);
|
|
1698
|
+
* res.status(response.status).json(response.body);
|
|
1699
|
+
* });
|
|
1700
|
+
* ```
|
|
1701
|
+
*/
|
|
1702
|
+
declare function toWebhookRequest<TBody = unknown>(req: ExpressLikeRequest): WebhookRequest<TBody>;
|
|
1703
|
+
/**
|
|
1704
|
+
* Send a WebhookResponse using an Express-like response object.
|
|
1705
|
+
*
|
|
1706
|
+
* @param res - Express-like response object
|
|
1707
|
+
* @param response - WebhookResponse to send
|
|
1708
|
+
*
|
|
1709
|
+
* @example
|
|
1710
|
+
* ```typescript
|
|
1711
|
+
* app.post('/checkout', async (req, res) => {
|
|
1712
|
+
* const response = await handler(toWebhookRequest(req));
|
|
1713
|
+
* sendWebhookResponse(res, response);
|
|
1714
|
+
* });
|
|
1715
|
+
* ```
|
|
1716
|
+
*/
|
|
1717
|
+
declare function sendWebhookResponse(res: ExpressLikeResponse, response: WebhookResponse): void;
|
|
1718
|
+
/**
|
|
1719
|
+
* Create an Express-compatible middleware from a webhook handler.
|
|
1720
|
+
*
|
|
1721
|
+
* @param handler - Webhook handler function
|
|
1722
|
+
* @returns Express middleware function
|
|
1723
|
+
*
|
|
1724
|
+
* @example
|
|
1725
|
+
* ```typescript
|
|
1726
|
+
* const handler = createWebhookHandler(workflow, workflowFn, config);
|
|
1727
|
+
* const middleware = createExpressHandler(handler);
|
|
1728
|
+
* app.post('/checkout', middleware);
|
|
1729
|
+
* ```
|
|
1730
|
+
*/
|
|
1731
|
+
declare function createExpressHandler<TBody = unknown>(handler: WebhookHandler<TBody>): (req: ExpressLikeRequest, res: ExpressLikeResponse) => Promise<void>;
|
|
1732
|
+
/**
|
|
1733
|
+
* Create a validation error.
|
|
1734
|
+
*
|
|
1735
|
+
* @param message - Error message
|
|
1736
|
+
* @param field - Optional field name
|
|
1737
|
+
* @param details - Optional additional details
|
|
1738
|
+
* @returns ValidationError
|
|
1739
|
+
*/
|
|
1740
|
+
declare function validationError(message: string, field?: string, details?: unknown): ValidationError;
|
|
1741
|
+
/**
|
|
1742
|
+
* Create a required field validator.
|
|
1743
|
+
*
|
|
1744
|
+
* @param fields - Field names to validate
|
|
1745
|
+
* @returns Validation function
|
|
1746
|
+
*
|
|
1747
|
+
* @example
|
|
1748
|
+
* ```typescript
|
|
1749
|
+
* const validateRequired = requireFields(['email', 'password']);
|
|
1750
|
+
*
|
|
1751
|
+
* const validateInput = (req) => {
|
|
1752
|
+
* const result = validateRequired(req.body);
|
|
1753
|
+
* if (!result.ok) return result;
|
|
1754
|
+
* return ok(req.body as LoginInput);
|
|
1755
|
+
* };
|
|
1756
|
+
* ```
|
|
1757
|
+
*/
|
|
1758
|
+
declare function requireFields(fields: string[]): (body: Record<string, unknown>) => ValidationResult<void, ValidationError>;
|
|
1759
|
+
/**
|
|
1760
|
+
* Compose multiple validators into a single validator.
|
|
1761
|
+
*
|
|
1762
|
+
* @param validators - Validators to compose
|
|
1763
|
+
* @returns Combined validator function
|
|
1764
|
+
*
|
|
1765
|
+
* @example
|
|
1766
|
+
* ```typescript
|
|
1767
|
+
* const validate = composeValidators(
|
|
1768
|
+
* requireFields(['email', 'password']),
|
|
1769
|
+
* validateEmailFormat,
|
|
1770
|
+
* validatePasswordStrength
|
|
1771
|
+
* );
|
|
1772
|
+
* ```
|
|
1773
|
+
*/
|
|
1774
|
+
declare function composeValidators<T>(...validators: Array<(input: T) => ValidationResult<void, ValidationError>>): (input: T) => ValidationResult<void, ValidationError>;
|
|
1775
|
+
/**
|
|
1776
|
+
* Generic event message for queue-based triggers.
|
|
1777
|
+
*/
|
|
1778
|
+
interface EventMessage<T = unknown> {
|
|
1779
|
+
/** Unique message ID */
|
|
1780
|
+
id: string;
|
|
1781
|
+
/** Event type/name */
|
|
1782
|
+
type: string;
|
|
1783
|
+
/** Event payload */
|
|
1784
|
+
payload: T;
|
|
1785
|
+
/** Event metadata */
|
|
1786
|
+
metadata?: {
|
|
1787
|
+
timestamp?: number;
|
|
1788
|
+
source?: string;
|
|
1789
|
+
correlationId?: string;
|
|
1790
|
+
[key: string]: unknown;
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Result of processing an event.
|
|
1795
|
+
*/
|
|
1796
|
+
interface EventProcessingResult {
|
|
1797
|
+
/** Whether the event was processed successfully */
|
|
1798
|
+
success: boolean;
|
|
1799
|
+
/** Should the message be acknowledged (removed from queue)? */
|
|
1800
|
+
ack: boolean;
|
|
1801
|
+
/** Optional error details */
|
|
1802
|
+
error?: {
|
|
1803
|
+
type: string;
|
|
1804
|
+
message?: string;
|
|
1805
|
+
retryable?: boolean;
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Configuration for event trigger handlers.
|
|
1810
|
+
*/
|
|
1811
|
+
interface EventTriggerConfig<TPayload, TOutput, TError> {
|
|
1812
|
+
/** Validate the event payload */
|
|
1813
|
+
validatePayload: (event: EventMessage<TPayload>) => ValidationResult<TPayload, ValidationError>;
|
|
1814
|
+
/** Map workflow result to processing result */
|
|
1815
|
+
mapResult: (result: Result<TOutput, TError | UnexpectedError>, event: EventMessage<TPayload>) => EventProcessingResult;
|
|
1816
|
+
/** Optional: Determine if error is retryable */
|
|
1817
|
+
isRetryable?: (error: TError | UnexpectedError) => boolean;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Event handler function type.
|
|
1821
|
+
*/
|
|
1822
|
+
type EventHandler<TPayload = unknown> = (event: EventMessage<TPayload>) => Promise<EventProcessingResult>;
|
|
1823
|
+
/**
|
|
1824
|
+
* Create an event handler for queue-based triggers.
|
|
1825
|
+
*
|
|
1826
|
+
* @example
|
|
1827
|
+
* ```typescript
|
|
1828
|
+
* const handler = createEventHandler(
|
|
1829
|
+
* checkoutWorkflow,
|
|
1830
|
+
* async (step, deps, payload: CheckoutPayload) => {
|
|
1831
|
+
* const charge = await step(() => deps.chargeCard(payload.amount));
|
|
1832
|
+
* return { chargeId: charge.id };
|
|
1833
|
+
* },
|
|
1834
|
+
* {
|
|
1835
|
+
* validatePayload: (event) => {
|
|
1836
|
+
* if (!event.payload.amount) {
|
|
1837
|
+
* return err({ type: 'VALIDATION_ERROR', message: 'Missing amount' });
|
|
1838
|
+
* }
|
|
1839
|
+
* return ok(event.payload);
|
|
1840
|
+
* },
|
|
1841
|
+
* mapResult: (result) => ({
|
|
1842
|
+
* success: result.ok,
|
|
1843
|
+
* ack: result.ok || !isRetryableError(result.error),
|
|
1844
|
+
* error: result.ok ? undefined : { type: String(result.error) },
|
|
1845
|
+
* }),
|
|
1846
|
+
* }
|
|
1847
|
+
* );
|
|
1848
|
+
*
|
|
1849
|
+
* // Use with SQS, RabbitMQ, etc.
|
|
1850
|
+
* queue.consume(async (message) => {
|
|
1851
|
+
* const result = await handler(message);
|
|
1852
|
+
* if (result.ack) await message.ack();
|
|
1853
|
+
* else await message.nack();
|
|
1854
|
+
* });
|
|
1855
|
+
* ```
|
|
1856
|
+
*/
|
|
1857
|
+
declare function createEventHandler<TPayload, TOutput, TError, TDeps = unknown>(workflow: Workflow<TError, TDeps> | WorkflowStrict<TError, unknown, TDeps>, workflowFn: (step: Parameters<Parameters<Workflow<TError, TDeps>>[1]>[0], deps: TDeps, payload: TPayload) => TOutput | Promise<TOutput>, config: EventTriggerConfig<TPayload, TOutput, TError>): EventHandler<TPayload>;
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* @jagreehal/workflow/policies
|
|
1861
|
+
*
|
|
1862
|
+
* Policy-Driven Step Middleware - Reusable bundles of StepOptions
|
|
1863
|
+
* that can be composed and applied per-workflow or per-step.
|
|
1864
|
+
*/
|
|
1865
|
+
|
|
1866
|
+
/**
|
|
1867
|
+
* A policy is a partial StepOptions that can be merged with other policies.
|
|
1868
|
+
*/
|
|
1869
|
+
type Policy = Partial<StepOptions>;
|
|
1870
|
+
/**
|
|
1871
|
+
* A policy factory that creates policies based on context.
|
|
1872
|
+
*/
|
|
1873
|
+
type PolicyFactory<T = void> = T extends void ? () => Policy : (context: T) => Policy;
|
|
1874
|
+
/**
|
|
1875
|
+
* Named policy with metadata.
|
|
1876
|
+
*/
|
|
1877
|
+
interface NamedPolicy {
|
|
1878
|
+
name: string;
|
|
1879
|
+
policy: Policy;
|
|
1880
|
+
description?: string;
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Merge multiple policies into a single StepOptions object.
|
|
1884
|
+
* Later policies override earlier ones for conflicting properties.
|
|
1885
|
+
* Retry and timeout options are deep-merged.
|
|
1886
|
+
*
|
|
1887
|
+
* @param policies - Policies to merge (in order of precedence)
|
|
1888
|
+
* @returns Merged StepOptions
|
|
1889
|
+
*
|
|
1890
|
+
* @example
|
|
1891
|
+
* ```typescript
|
|
1892
|
+
* const merged = mergePolicies(
|
|
1893
|
+
* timeoutPolicies.api, // timeout: 5000ms
|
|
1894
|
+
* retryPolicies.transient, // retry: 3 attempts
|
|
1895
|
+
* { name: 'fetch-user' } // name override
|
|
1896
|
+
* );
|
|
1897
|
+
* ```
|
|
1898
|
+
*/
|
|
1899
|
+
declare function mergePolicies(...policies: Policy[]): StepOptions;
|
|
1900
|
+
/**
|
|
1901
|
+
* Create a policy applier that merges base policies with step-specific options.
|
|
1902
|
+
*
|
|
1903
|
+
* @param basePolicies - Base policies to apply to all steps
|
|
1904
|
+
* @returns A function that applies policies to step options
|
|
1905
|
+
*
|
|
1906
|
+
* @example
|
|
1907
|
+
* ```typescript
|
|
1908
|
+
* const applyPolicy = createPolicyApplier(
|
|
1909
|
+
* timeoutPolicies.api,
|
|
1910
|
+
* retryPolicies.transient
|
|
1911
|
+
* );
|
|
1912
|
+
*
|
|
1913
|
+
* // In workflow
|
|
1914
|
+
* const user = await step(
|
|
1915
|
+
* () => fetchUser(id),
|
|
1916
|
+
* applyPolicy({ name: 'fetch-user', key: 'user:' + id })
|
|
1917
|
+
* );
|
|
1918
|
+
* ```
|
|
1919
|
+
*/
|
|
1920
|
+
declare function createPolicyApplier(...basePolicies: Policy[]): (stepOptions?: StepOptions | string) => StepOptions;
|
|
1921
|
+
/**
|
|
1922
|
+
* Create a named policy bundle for reuse across workflows.
|
|
1923
|
+
*
|
|
1924
|
+
* @param name - Policy bundle name
|
|
1925
|
+
* @param policies - Policies to include in the bundle
|
|
1926
|
+
* @returns Named policy object
|
|
1927
|
+
*/
|
|
1928
|
+
declare function createPolicyBundle(name: string, ...policies: Policy[]): NamedPolicy;
|
|
1929
|
+
/**
|
|
1930
|
+
* Create a retry policy with the given options.
|
|
1931
|
+
*/
|
|
1932
|
+
declare function retryPolicy(options: RetryOptions): Policy;
|
|
1933
|
+
/**
|
|
1934
|
+
* Pre-built retry policies for common scenarios.
|
|
1935
|
+
*/
|
|
1936
|
+
declare const retryPolicies: {
|
|
1937
|
+
/**
|
|
1938
|
+
* No retry - fail immediately on error.
|
|
1939
|
+
*/
|
|
1940
|
+
readonly none: Partial<StepOptions>;
|
|
1941
|
+
/**
|
|
1942
|
+
* Quick retry for transient errors (3 attempts, fast backoff).
|
|
1943
|
+
*/
|
|
1944
|
+
readonly transient: Partial<StepOptions>;
|
|
1945
|
+
/**
|
|
1946
|
+
* Standard retry for API calls (3 attempts, moderate backoff).
|
|
1947
|
+
*/
|
|
1948
|
+
readonly standard: Partial<StepOptions>;
|
|
1949
|
+
/**
|
|
1950
|
+
* Aggressive retry for critical operations (5 attempts, longer backoff).
|
|
1951
|
+
*/
|
|
1952
|
+
readonly aggressive: Partial<StepOptions>;
|
|
1953
|
+
/**
|
|
1954
|
+
* Fixed interval retry (useful for polling).
|
|
1955
|
+
*/
|
|
1956
|
+
readonly fixed: (attempts: number, delayMs: number) => Policy;
|
|
1957
|
+
/**
|
|
1958
|
+
* Linear backoff retry.
|
|
1959
|
+
*/
|
|
1960
|
+
readonly linear: (attempts: number, initialDelay: number) => Policy;
|
|
1961
|
+
/**
|
|
1962
|
+
* Custom retry policy builder.
|
|
1963
|
+
*/
|
|
1964
|
+
readonly custom: (options: Partial<RetryOptions> & {
|
|
1965
|
+
attempts: number;
|
|
1966
|
+
}) => Policy;
|
|
1967
|
+
};
|
|
1968
|
+
/**
|
|
1969
|
+
* Create a timeout policy with the given options.
|
|
1970
|
+
*/
|
|
1971
|
+
declare function timeoutPolicy(options: TimeoutOptions): Policy;
|
|
1972
|
+
/**
|
|
1973
|
+
* Pre-built timeout policies for common scenarios.
|
|
1974
|
+
*/
|
|
1975
|
+
declare const timeoutPolicies: {
|
|
1976
|
+
/**
|
|
1977
|
+
* No timeout.
|
|
1978
|
+
*/
|
|
1979
|
+
readonly none: Policy;
|
|
1980
|
+
/**
|
|
1981
|
+
* Fast timeout for quick operations (1 second).
|
|
1982
|
+
*/
|
|
1983
|
+
readonly fast: Partial<StepOptions>;
|
|
1984
|
+
/**
|
|
1985
|
+
* Standard API timeout (5 seconds).
|
|
1986
|
+
*/
|
|
1987
|
+
readonly api: Partial<StepOptions>;
|
|
1988
|
+
/**
|
|
1989
|
+
* Extended timeout for slower operations (30 seconds).
|
|
1990
|
+
*/
|
|
1991
|
+
readonly extended: Partial<StepOptions>;
|
|
1992
|
+
/**
|
|
1993
|
+
* Long timeout for batch operations (2 minutes).
|
|
1994
|
+
*/
|
|
1995
|
+
readonly long: Partial<StepOptions>;
|
|
1996
|
+
/**
|
|
1997
|
+
* Custom timeout in milliseconds.
|
|
1998
|
+
*/
|
|
1999
|
+
readonly ms: (ms: number) => Policy;
|
|
2000
|
+
/**
|
|
2001
|
+
* Custom timeout in seconds.
|
|
2002
|
+
*/
|
|
2003
|
+
readonly seconds: (seconds: number) => Policy;
|
|
2004
|
+
/**
|
|
2005
|
+
* Timeout with custom error.
|
|
2006
|
+
*/
|
|
2007
|
+
readonly withError: <E>(ms: number, error: E) => Policy;
|
|
2008
|
+
/**
|
|
2009
|
+
* Timeout with AbortSignal support.
|
|
2010
|
+
*/
|
|
2011
|
+
readonly withSignal: (ms: number) => Policy;
|
|
2012
|
+
};
|
|
2013
|
+
/**
|
|
2014
|
+
* Pre-built combined policies for common service patterns.
|
|
2015
|
+
*/
|
|
2016
|
+
declare const servicePolicies: {
|
|
2017
|
+
/**
|
|
2018
|
+
* Policy for external HTTP APIs.
|
|
2019
|
+
* - 5 second timeout
|
|
2020
|
+
* - 3 retries with exponential backoff
|
|
2021
|
+
*/
|
|
2022
|
+
readonly httpApi: StepOptions;
|
|
2023
|
+
/**
|
|
2024
|
+
* Policy for database operations.
|
|
2025
|
+
* - 30 second timeout
|
|
2026
|
+
* - 2 retries for transient errors
|
|
2027
|
+
*/
|
|
2028
|
+
readonly database: StepOptions;
|
|
2029
|
+
/**
|
|
2030
|
+
* Policy for cache operations.
|
|
2031
|
+
* - 1 second timeout
|
|
2032
|
+
* - No retry (cache misses are not errors)
|
|
2033
|
+
*/
|
|
2034
|
+
readonly cache: StepOptions;
|
|
2035
|
+
/**
|
|
2036
|
+
* Policy for message queue operations.
|
|
2037
|
+
* - 30 second timeout
|
|
2038
|
+
* - 5 retries with longer backoff
|
|
2039
|
+
*/
|
|
2040
|
+
readonly messageQueue: StepOptions;
|
|
2041
|
+
/**
|
|
2042
|
+
* Policy for file operations.
|
|
2043
|
+
* - 2 minute timeout
|
|
2044
|
+
* - 3 retries
|
|
2045
|
+
*/
|
|
2046
|
+
readonly fileSystem: StepOptions;
|
|
2047
|
+
/**
|
|
2048
|
+
* Policy for third-party services with rate limits.
|
|
2049
|
+
* - 10 second timeout
|
|
2050
|
+
* - 5 retries with linear backoff
|
|
2051
|
+
*/
|
|
2052
|
+
readonly rateLimited: StepOptions;
|
|
2053
|
+
};
|
|
2054
|
+
/**
|
|
2055
|
+
* Options for withPolicies workflow wrapper.
|
|
2056
|
+
*/
|
|
2057
|
+
interface WithPoliciesOptions {
|
|
2058
|
+
/**
|
|
2059
|
+
* Base policies applied to all steps.
|
|
2060
|
+
*/
|
|
2061
|
+
policies: Policy[];
|
|
2062
|
+
/**
|
|
2063
|
+
* Step-specific policy overrides by name or key pattern.
|
|
2064
|
+
*/
|
|
2065
|
+
overrides?: Record<string, Policy>;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Create step options with policies applied.
|
|
2069
|
+
* This is a helper for applying policies inline.
|
|
2070
|
+
*
|
|
2071
|
+
* @param policies - Policies to apply
|
|
2072
|
+
* @param stepOptions - Step-specific options
|
|
2073
|
+
* @returns Merged StepOptions
|
|
2074
|
+
*
|
|
2075
|
+
* @example
|
|
2076
|
+
* ```typescript
|
|
2077
|
+
* const user = await step(
|
|
2078
|
+
* () => fetchUser(id),
|
|
2079
|
+
* withPolicy(servicePolicies.httpApi, { name: 'fetch-user' })
|
|
2080
|
+
* );
|
|
2081
|
+
* ```
|
|
2082
|
+
*/
|
|
2083
|
+
declare function withPolicy(policy: Policy, stepOptions?: StepOptions | string): StepOptions;
|
|
2084
|
+
/**
|
|
2085
|
+
* Create step options with multiple policies applied.
|
|
2086
|
+
*
|
|
2087
|
+
* @param policies - Policies to apply (in order)
|
|
2088
|
+
* @param stepOptions - Step-specific options
|
|
2089
|
+
* @returns Merged StepOptions
|
|
2090
|
+
*
|
|
2091
|
+
* @example
|
|
2092
|
+
* ```typescript
|
|
2093
|
+
* const user = await step(
|
|
2094
|
+
* () => fetchUser(id),
|
|
2095
|
+
* withPolicies([timeoutPolicies.api, retryPolicies.standard], { name: 'fetch-user' })
|
|
2096
|
+
* );
|
|
2097
|
+
* ```
|
|
2098
|
+
*/
|
|
2099
|
+
declare function withPolicies(policies: Policy[], stepOptions?: StepOptions | string): StepOptions;
|
|
2100
|
+
/**
|
|
2101
|
+
* Create a policy that applies conditionally.
|
|
2102
|
+
*
|
|
2103
|
+
* @param condition - Condition to check
|
|
2104
|
+
* @param policy - Policy to apply if condition is true
|
|
2105
|
+
* @param elsePolicy - Policy to apply if condition is false (optional)
|
|
2106
|
+
* @returns The selected policy
|
|
2107
|
+
*
|
|
2108
|
+
* @example
|
|
2109
|
+
* ```typescript
|
|
2110
|
+
* const policy = conditionalPolicy(
|
|
2111
|
+
* isProduction,
|
|
2112
|
+
* servicePolicies.httpApi, // Use in production
|
|
2113
|
+
* retryPolicies.none // Skip in development
|
|
2114
|
+
* );
|
|
2115
|
+
* ```
|
|
2116
|
+
*/
|
|
2117
|
+
declare function conditionalPolicy(condition: boolean, policy: Policy, elsePolicy?: Policy): Policy;
|
|
2118
|
+
/**
|
|
2119
|
+
* Create a policy based on environment.
|
|
2120
|
+
*
|
|
2121
|
+
* @param envPolicies - Map of environment names to policies
|
|
2122
|
+
* @param currentEnv - Current environment (defaults to NODE_ENV)
|
|
2123
|
+
* @param defaultPolicy - Default policy if environment not found
|
|
2124
|
+
* @returns The selected policy
|
|
2125
|
+
*
|
|
2126
|
+
* @example
|
|
2127
|
+
* ```typescript
|
|
2128
|
+
* const policy = envPolicy({
|
|
2129
|
+
* production: servicePolicies.httpApi,
|
|
2130
|
+
* development: retryPolicies.none,
|
|
2131
|
+
* test: retryPolicies.none,
|
|
2132
|
+
* });
|
|
2133
|
+
* ```
|
|
2134
|
+
*/
|
|
2135
|
+
declare function envPolicy(envPolicies: Record<string, Policy>, currentEnv?: string, defaultPolicy?: Policy): Policy;
|
|
2136
|
+
/**
|
|
2137
|
+
* A registry for managing named policies.
|
|
2138
|
+
*/
|
|
2139
|
+
interface PolicyRegistry {
|
|
2140
|
+
/**
|
|
2141
|
+
* Register a named policy.
|
|
2142
|
+
*/
|
|
2143
|
+
register(name: string, policy: Policy): void;
|
|
2144
|
+
/**
|
|
2145
|
+
* Get a policy by name.
|
|
2146
|
+
*/
|
|
2147
|
+
get(name: string): Policy | undefined;
|
|
2148
|
+
/**
|
|
2149
|
+
* Check if a policy exists.
|
|
2150
|
+
*/
|
|
2151
|
+
has(name: string): boolean;
|
|
2152
|
+
/**
|
|
2153
|
+
* Get all registered policy names.
|
|
2154
|
+
*/
|
|
2155
|
+
names(): string[];
|
|
2156
|
+
/**
|
|
2157
|
+
* Create step options using a registered policy.
|
|
2158
|
+
*/
|
|
2159
|
+
apply(policyName: string, stepOptions?: StepOptions | string): StepOptions;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Create a policy registry for managing named policies.
|
|
2163
|
+
*
|
|
2164
|
+
* @returns PolicyRegistry instance
|
|
2165
|
+
*
|
|
2166
|
+
* @example
|
|
2167
|
+
* ```typescript
|
|
2168
|
+
* const registry = createPolicyRegistry();
|
|
2169
|
+
*
|
|
2170
|
+
* // Register policies
|
|
2171
|
+
* registry.register('api', servicePolicies.httpApi);
|
|
2172
|
+
* registry.register('db', servicePolicies.database);
|
|
2173
|
+
*
|
|
2174
|
+
* // Use in workflow
|
|
2175
|
+
* const user = await step(
|
|
2176
|
+
* () => fetchUser(id),
|
|
2177
|
+
* registry.apply('api', { name: 'fetch-user' })
|
|
2178
|
+
* );
|
|
2179
|
+
* ```
|
|
2180
|
+
*/
|
|
2181
|
+
declare function createPolicyRegistry(): PolicyRegistry;
|
|
2182
|
+
/**
|
|
2183
|
+
* Fluent builder for constructing step options.
|
|
2184
|
+
*/
|
|
2185
|
+
interface StepOptionsBuilder {
|
|
2186
|
+
/**
|
|
2187
|
+
* Set step name.
|
|
2188
|
+
*/
|
|
2189
|
+
name(name: string): StepOptionsBuilder;
|
|
2190
|
+
/**
|
|
2191
|
+
* Set step key for caching.
|
|
2192
|
+
*/
|
|
2193
|
+
key(key: string): StepOptionsBuilder;
|
|
2194
|
+
/**
|
|
2195
|
+
* Apply a policy.
|
|
2196
|
+
*/
|
|
2197
|
+
policy(policy: Policy): StepOptionsBuilder;
|
|
2198
|
+
/**
|
|
2199
|
+
* Set timeout in milliseconds.
|
|
2200
|
+
*/
|
|
2201
|
+
timeout(ms: number): StepOptionsBuilder;
|
|
2202
|
+
/**
|
|
2203
|
+
* Set retry options.
|
|
2204
|
+
*/
|
|
2205
|
+
retry(options: RetryOptions): StepOptionsBuilder;
|
|
2206
|
+
/**
|
|
2207
|
+
* Set retry attempts (with default exponential backoff).
|
|
2208
|
+
*/
|
|
2209
|
+
retries(attempts: number): StepOptionsBuilder;
|
|
2210
|
+
/**
|
|
2211
|
+
* Build the final StepOptions.
|
|
2212
|
+
*/
|
|
2213
|
+
build(): StepOptions;
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Create a fluent builder for step options.
|
|
2217
|
+
*
|
|
2218
|
+
* @returns StepOptionsBuilder instance
|
|
2219
|
+
*
|
|
2220
|
+
* @example
|
|
2221
|
+
* ```typescript
|
|
2222
|
+
* const options = stepOptions()
|
|
2223
|
+
* .name('fetch-user')
|
|
2224
|
+
* .key('user:123')
|
|
2225
|
+
* .timeout(5000)
|
|
2226
|
+
* .retries(3)
|
|
2227
|
+
* .build();
|
|
2228
|
+
*
|
|
2229
|
+
* const user = await step(() => fetchUser(id), options);
|
|
2230
|
+
* ```
|
|
2231
|
+
*/
|
|
2232
|
+
declare function stepOptions(): StepOptionsBuilder;
|
|
2233
|
+
|
|
2234
|
+
/**
|
|
2235
|
+
* @jagreehal/workflow/persistence
|
|
2236
|
+
*
|
|
2237
|
+
* Pluggable Persistence Adapters for StepCache and ResumeState.
|
|
2238
|
+
* Provides adapters for Redis, file system, and in-memory storage,
|
|
2239
|
+
* plus helpers for JSON-safe serialization of causes.
|
|
2240
|
+
*/
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* JSON-safe representation of a Result.
|
|
2244
|
+
*/
|
|
2245
|
+
interface SerializedResult {
|
|
2246
|
+
ok: boolean;
|
|
2247
|
+
value?: unknown;
|
|
2248
|
+
error?: unknown;
|
|
2249
|
+
cause?: SerializedCause;
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* JSON-safe representation of a cause value.
|
|
2253
|
+
* Handles Error objects and other non-JSON-safe types.
|
|
2254
|
+
*/
|
|
2255
|
+
interface SerializedCause {
|
|
2256
|
+
type: "error" | "value" | "undefined";
|
|
2257
|
+
errorName?: string;
|
|
2258
|
+
errorMessage?: string;
|
|
2259
|
+
errorStack?: string;
|
|
2260
|
+
value?: unknown;
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* JSON-safe representation of StepFailureMeta.
|
|
2264
|
+
*/
|
|
2265
|
+
interface SerializedMeta {
|
|
2266
|
+
origin: "result" | "throw";
|
|
2267
|
+
resultCause?: SerializedCause;
|
|
2268
|
+
thrown?: SerializedCause;
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* JSON-safe representation of a ResumeStateEntry.
|
|
2272
|
+
*/
|
|
2273
|
+
interface SerializedEntry {
|
|
2274
|
+
result: SerializedResult;
|
|
2275
|
+
meta?: SerializedMeta;
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* JSON-safe representation of ResumeState.
|
|
2279
|
+
*/
|
|
2280
|
+
interface SerializedState {
|
|
2281
|
+
version: number;
|
|
2282
|
+
entries: Record<string, SerializedEntry>;
|
|
2283
|
+
metadata?: Record<string, unknown>;
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Serialize a cause value to a JSON-safe format.
|
|
2287
|
+
*/
|
|
2288
|
+
declare function serializeCause(cause: unknown): SerializedCause;
|
|
2289
|
+
/**
|
|
2290
|
+
* Deserialize a cause value from JSON-safe format.
|
|
2291
|
+
*/
|
|
2292
|
+
declare function deserializeCause(serialized: SerializedCause): unknown;
|
|
2293
|
+
/**
|
|
2294
|
+
* Serialize a Result to a JSON-safe format.
|
|
2295
|
+
*/
|
|
2296
|
+
declare function serializeResult(result: Result<unknown, unknown, unknown>): SerializedResult;
|
|
2297
|
+
/**
|
|
2298
|
+
* Deserialize a Result from JSON-safe format.
|
|
2299
|
+
*/
|
|
2300
|
+
declare function deserializeResult(serialized: SerializedResult): Result<unknown, unknown, unknown>;
|
|
2301
|
+
/**
|
|
2302
|
+
* Serialize StepFailureMeta to a JSON-safe format.
|
|
2303
|
+
*/
|
|
2304
|
+
declare function serializeMeta(meta: StepFailureMeta): SerializedMeta;
|
|
2305
|
+
/**
|
|
2306
|
+
* Deserialize StepFailureMeta from JSON-safe format.
|
|
2307
|
+
*/
|
|
2308
|
+
declare function deserializeMeta(serialized: SerializedMeta): StepFailureMeta;
|
|
2309
|
+
/**
|
|
2310
|
+
* Serialize a ResumeStateEntry to a JSON-safe format.
|
|
2311
|
+
*/
|
|
2312
|
+
declare function serializeEntry(entry: ResumeStateEntry): SerializedEntry;
|
|
2313
|
+
/**
|
|
2314
|
+
* Deserialize a ResumeStateEntry from JSON-safe format.
|
|
2315
|
+
*/
|
|
2316
|
+
declare function deserializeEntry(serialized: SerializedEntry): ResumeStateEntry;
|
|
2317
|
+
/**
|
|
2318
|
+
* Serialize ResumeState to a JSON-safe format.
|
|
2319
|
+
*/
|
|
2320
|
+
declare function serializeState(state: ResumeState, metadata?: Record<string, unknown>): SerializedState;
|
|
2321
|
+
/**
|
|
2322
|
+
* Deserialize ResumeState from JSON-safe format.
|
|
2323
|
+
*/
|
|
2324
|
+
declare function deserializeState(serialized: SerializedState): ResumeState;
|
|
2325
|
+
/**
|
|
2326
|
+
* Convert ResumeState to a JSON string.
|
|
2327
|
+
*/
|
|
2328
|
+
declare function stringifyState(state: ResumeState, metadata?: Record<string, unknown>): string;
|
|
2329
|
+
/**
|
|
2330
|
+
* Parse ResumeState from a JSON string.
|
|
2331
|
+
*/
|
|
2332
|
+
declare function parseState(json: string): ResumeState;
|
|
2333
|
+
/**
|
|
2334
|
+
* Options for the in-memory cache adapter.
|
|
2335
|
+
*/
|
|
2336
|
+
interface MemoryCacheOptions {
|
|
2337
|
+
/**
|
|
2338
|
+
* Maximum number of entries to store.
|
|
2339
|
+
* Oldest entries are evicted when limit is reached.
|
|
2340
|
+
*/
|
|
2341
|
+
maxSize?: number;
|
|
2342
|
+
/**
|
|
2343
|
+
* Time-to-live in milliseconds.
|
|
2344
|
+
* Entries are automatically removed after this duration.
|
|
2345
|
+
*/
|
|
2346
|
+
ttl?: number;
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Create an in-memory StepCache with optional LRU eviction and TTL.
|
|
2350
|
+
*
|
|
2351
|
+
* @param options - Cache options
|
|
2352
|
+
* @returns StepCache implementation
|
|
2353
|
+
*
|
|
2354
|
+
* @example
|
|
2355
|
+
* ```typescript
|
|
2356
|
+
* const cache = createMemoryCache({ maxSize: 1000, ttl: 60000 });
|
|
2357
|
+
* const workflow = createWorkflow(deps, { cache });
|
|
2358
|
+
* ```
|
|
2359
|
+
*/
|
|
2360
|
+
declare function createMemoryCache(options?: MemoryCacheOptions): StepCache;
|
|
2361
|
+
/**
|
|
2362
|
+
* Options for the file system cache adapter.
|
|
2363
|
+
*/
|
|
2364
|
+
interface FileCacheOptions {
|
|
2365
|
+
/**
|
|
2366
|
+
* Directory to store cache files.
|
|
2367
|
+
*/
|
|
2368
|
+
directory: string;
|
|
2369
|
+
/**
|
|
2370
|
+
* File extension for cache files.
|
|
2371
|
+
* @default '.json'
|
|
2372
|
+
*/
|
|
2373
|
+
extension?: string;
|
|
2374
|
+
/**
|
|
2375
|
+
* Custom file system interface (for testing or custom implementations).
|
|
2376
|
+
*/
|
|
2377
|
+
fs?: FileSystemInterface;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Minimal file system interface for cache operations.
|
|
2381
|
+
*/
|
|
2382
|
+
interface FileSystemInterface {
|
|
2383
|
+
readFile(path: string): Promise<string>;
|
|
2384
|
+
writeFile(path: string, data: string): Promise<void>;
|
|
2385
|
+
unlink(path: string): Promise<void>;
|
|
2386
|
+
exists(path: string): Promise<boolean>;
|
|
2387
|
+
readdir(path: string): Promise<string[]>;
|
|
2388
|
+
mkdir(path: string, options?: {
|
|
2389
|
+
recursive?: boolean;
|
|
2390
|
+
}): Promise<void>;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Create a file system-based StepCache.
|
|
2394
|
+
* Each step result is stored as a separate JSON file.
|
|
2395
|
+
*
|
|
2396
|
+
* @param options - Cache options
|
|
2397
|
+
* @returns StepCache implementation (async operations wrapped in sync interface)
|
|
2398
|
+
*
|
|
2399
|
+
* @example
|
|
2400
|
+
* ```typescript
|
|
2401
|
+
* import * as fs from 'fs/promises';
|
|
2402
|
+
*
|
|
2403
|
+
* const cache = createFileCache({
|
|
2404
|
+
* directory: './workflow-cache',
|
|
2405
|
+
* fs: {
|
|
2406
|
+
* readFile: (path) => fs.readFile(path, 'utf-8'),
|
|
2407
|
+
* writeFile: (path, data) => fs.writeFile(path, data, 'utf-8'),
|
|
2408
|
+
* unlink: fs.unlink,
|
|
2409
|
+
* exists: async (path) => fs.access(path).then(() => true).catch(() => false),
|
|
2410
|
+
* readdir: fs.readdir,
|
|
2411
|
+
* mkdir: fs.mkdir,
|
|
2412
|
+
* },
|
|
2413
|
+
* });
|
|
2414
|
+
* ```
|
|
2415
|
+
*/
|
|
2416
|
+
declare function createFileCache(options: FileCacheOptions): StepCache & {
|
|
2417
|
+
/** Initialize the cache directory. Call before using the cache. */
|
|
2418
|
+
init(): Promise<void>;
|
|
2419
|
+
/** Get a result asynchronously. */
|
|
2420
|
+
getAsync(key: string): Promise<Result<unknown, unknown, unknown> | undefined>;
|
|
2421
|
+
/** Set a result asynchronously. */
|
|
2422
|
+
setAsync(key: string, result: Result<unknown, unknown, unknown>): Promise<void>;
|
|
2423
|
+
/** Delete a result asynchronously. */
|
|
2424
|
+
deleteAsync(key: string): Promise<boolean>;
|
|
2425
|
+
/** Clear all results asynchronously. */
|
|
2426
|
+
clearAsync(): Promise<void>;
|
|
2427
|
+
};
|
|
2428
|
+
/**
|
|
2429
|
+
* Generic key-value store interface.
|
|
2430
|
+
* Implement this for Redis, DynamoDB, etc.
|
|
2431
|
+
*/
|
|
2432
|
+
interface KeyValueStore {
|
|
2433
|
+
get(key: string): Promise<string | null>;
|
|
2434
|
+
set(key: string, value: string, options?: {
|
|
2435
|
+
ttl?: number;
|
|
2436
|
+
}): Promise<void>;
|
|
2437
|
+
delete(key: string): Promise<boolean>;
|
|
2438
|
+
exists(key: string): Promise<boolean>;
|
|
2439
|
+
keys(pattern: string): Promise<string[]>;
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Options for key-value store cache adapter.
|
|
2443
|
+
*/
|
|
2444
|
+
interface KVCacheOptions {
|
|
2445
|
+
/**
|
|
2446
|
+
* Key-value store implementation.
|
|
2447
|
+
*/
|
|
2448
|
+
store: KeyValueStore;
|
|
2449
|
+
/**
|
|
2450
|
+
* Key prefix for all cache entries.
|
|
2451
|
+
* @default 'workflow:'
|
|
2452
|
+
*/
|
|
2453
|
+
prefix?: string;
|
|
2454
|
+
/**
|
|
2455
|
+
* Time-to-live in seconds for cache entries.
|
|
2456
|
+
*/
|
|
2457
|
+
ttl?: number;
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Create a StepCache backed by a key-value store (Redis, DynamoDB, etc.).
|
|
2461
|
+
*
|
|
2462
|
+
* @param options - Cache options
|
|
2463
|
+
* @returns StepCache implementation with async methods
|
|
2464
|
+
*
|
|
2465
|
+
* @example
|
|
2466
|
+
* ```typescript
|
|
2467
|
+
* // With Redis
|
|
2468
|
+
* import { createClient } from 'redis';
|
|
2469
|
+
*
|
|
2470
|
+
* const redis = createClient();
|
|
2471
|
+
* await redis.connect();
|
|
2472
|
+
*
|
|
2473
|
+
* const cache = createKVCache({
|
|
2474
|
+
* store: {
|
|
2475
|
+
* get: (key) => redis.get(key),
|
|
2476
|
+
* set: (key, value, opts) => redis.set(key, value, { EX: opts?.ttl }),
|
|
2477
|
+
* delete: (key) => redis.del(key).then(n => n > 0),
|
|
2478
|
+
* exists: (key) => redis.exists(key).then(n => n > 0),
|
|
2479
|
+
* keys: (pattern) => redis.keys(pattern),
|
|
2480
|
+
* },
|
|
2481
|
+
* prefix: 'myapp:workflow:',
|
|
2482
|
+
* ttl: 3600, // 1 hour
|
|
2483
|
+
* });
|
|
2484
|
+
* ```
|
|
2485
|
+
*/
|
|
2486
|
+
declare function createKVCache(options: KVCacheOptions): StepCache & {
|
|
2487
|
+
/** Get a result asynchronously. */
|
|
2488
|
+
getAsync(key: string): Promise<Result<unknown, unknown, unknown> | undefined>;
|
|
2489
|
+
/** Set a result asynchronously. */
|
|
2490
|
+
setAsync(key: string, result: Result<unknown, unknown, unknown>): Promise<void>;
|
|
2491
|
+
/** Check if key exists asynchronously. */
|
|
2492
|
+
hasAsync(key: string): Promise<boolean>;
|
|
2493
|
+
/** Delete a result asynchronously. */
|
|
2494
|
+
deleteAsync(key: string): Promise<boolean>;
|
|
2495
|
+
/** Clear all results asynchronously. */
|
|
2496
|
+
clearAsync(): Promise<void>;
|
|
2497
|
+
};
|
|
2498
|
+
/**
|
|
2499
|
+
* Interface for persisting workflow state.
|
|
2500
|
+
*/
|
|
2501
|
+
interface StatePersistence {
|
|
2502
|
+
/**
|
|
2503
|
+
* Save workflow state.
|
|
2504
|
+
*/
|
|
2505
|
+
save(runId: string, state: ResumeState, metadata?: Record<string, unknown>): Promise<void>;
|
|
2506
|
+
/**
|
|
2507
|
+
* Load workflow state.
|
|
2508
|
+
*/
|
|
2509
|
+
load(runId: string): Promise<ResumeState | undefined>;
|
|
2510
|
+
/**
|
|
2511
|
+
* Delete workflow state.
|
|
2512
|
+
*/
|
|
2513
|
+
delete(runId: string): Promise<boolean>;
|
|
2514
|
+
/**
|
|
2515
|
+
* List all saved workflow IDs.
|
|
2516
|
+
*/
|
|
2517
|
+
list(): Promise<string[]>;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Create a state persistence adapter using a key-value store.
|
|
2521
|
+
*
|
|
2522
|
+
* @param store - Key-value store implementation
|
|
2523
|
+
* @param prefix - Key prefix for state entries
|
|
2524
|
+
* @returns StatePersistence implementation
|
|
2525
|
+
*/
|
|
2526
|
+
declare function createStatePersistence(store: KeyValueStore, prefix?: string): StatePersistence;
|
|
2527
|
+
/**
|
|
2528
|
+
* Create a cache that hydrates from persistent storage on first access.
|
|
2529
|
+
*
|
|
2530
|
+
* @param memoryCache - In-memory cache for fast access
|
|
2531
|
+
* @param persistence - Persistent storage for durability
|
|
2532
|
+
* @returns Hydrating cache implementation
|
|
2533
|
+
*/
|
|
2534
|
+
declare function createHydratingCache(memoryCache: StepCache, persistence: StatePersistence, runId: string): StepCache & {
|
|
2535
|
+
hydrate(): Promise<void>;
|
|
2536
|
+
};
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* @jagreehal/workflow/devtools
|
|
2540
|
+
*
|
|
2541
|
+
* Developer tools for workflow debugging, visualization, and analysis.
|
|
2542
|
+
* Provides timeline rendering, run diffing, and live visualization.
|
|
2543
|
+
*/
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* A recorded workflow run with events and metadata.
|
|
2547
|
+
*/
|
|
2548
|
+
interface WorkflowRun {
|
|
2549
|
+
/** Unique identifier for this run */
|
|
2550
|
+
id: string;
|
|
2551
|
+
/** Workflow name */
|
|
2552
|
+
name?: string;
|
|
2553
|
+
/** Start timestamp */
|
|
2554
|
+
startTime: number;
|
|
2555
|
+
/** End timestamp (undefined if still running) */
|
|
2556
|
+
endTime?: number;
|
|
2557
|
+
/** Duration in milliseconds */
|
|
2558
|
+
durationMs?: number;
|
|
2559
|
+
/** Whether the workflow succeeded */
|
|
2560
|
+
success?: boolean;
|
|
2561
|
+
/** Error if the workflow failed */
|
|
2562
|
+
error?: unknown;
|
|
2563
|
+
/** All events from this run */
|
|
2564
|
+
events: CollectableEvent[];
|
|
2565
|
+
/** Custom metadata */
|
|
2566
|
+
metadata?: Record<string, unknown>;
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Difference between two workflow runs.
|
|
2570
|
+
*/
|
|
2571
|
+
interface RunDiff {
|
|
2572
|
+
/** Steps that were added in the new run */
|
|
2573
|
+
added: StepDiff[];
|
|
2574
|
+
/** Steps that were removed from the new run */
|
|
2575
|
+
removed: StepDiff[];
|
|
2576
|
+
/** Steps that changed between runs */
|
|
2577
|
+
changed: StepDiff[];
|
|
2578
|
+
/** Steps that are identical */
|
|
2579
|
+
unchanged: string[];
|
|
2580
|
+
/** Overall status change */
|
|
2581
|
+
statusChange?: {
|
|
2582
|
+
from: "success" | "error" | "running";
|
|
2583
|
+
to: "success" | "error" | "running";
|
|
2584
|
+
};
|
|
2585
|
+
/** Duration change in milliseconds */
|
|
2586
|
+
durationChange?: number;
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Information about a step difference.
|
|
2590
|
+
*/
|
|
2591
|
+
interface StepDiff {
|
|
2592
|
+
/** Step name or key */
|
|
2593
|
+
step: string;
|
|
2594
|
+
/** Type of change */
|
|
2595
|
+
type: "added" | "removed" | "status" | "duration" | "error";
|
|
2596
|
+
/** Previous value (for changes) */
|
|
2597
|
+
from?: unknown;
|
|
2598
|
+
/** New value (for changes) */
|
|
2599
|
+
to?: unknown;
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Timeline entry for a step.
|
|
2603
|
+
*/
|
|
2604
|
+
interface TimelineEntry {
|
|
2605
|
+
/** Step name */
|
|
2606
|
+
name: string;
|
|
2607
|
+
/** Step key (if any) */
|
|
2608
|
+
key?: string;
|
|
2609
|
+
/** Start time (relative to workflow start) */
|
|
2610
|
+
startMs: number;
|
|
2611
|
+
/** End time (relative to workflow start) */
|
|
2612
|
+
endMs?: number;
|
|
2613
|
+
/** Duration in milliseconds */
|
|
2614
|
+
durationMs?: number;
|
|
2615
|
+
/** Step status */
|
|
2616
|
+
status: "pending" | "running" | "success" | "error" | "skipped" | "cached";
|
|
2617
|
+
/** Error if failed */
|
|
2618
|
+
error?: unknown;
|
|
2619
|
+
/** Parent scope (for nested steps) */
|
|
2620
|
+
parent?: string;
|
|
2621
|
+
/** Retry attempt number */
|
|
2622
|
+
attempt?: number;
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Devtools configuration options.
|
|
2626
|
+
*/
|
|
2627
|
+
interface DevtoolsOptions extends VisualizerOptions {
|
|
2628
|
+
/** Enable console logging of events */
|
|
2629
|
+
logEvents?: boolean;
|
|
2630
|
+
/** Maximum number of runs to keep in history */
|
|
2631
|
+
maxHistory?: number;
|
|
2632
|
+
/** Custom logger function */
|
|
2633
|
+
logger?: (message: string) => void;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Devtools instance for workflow debugging.
|
|
2637
|
+
*/
|
|
2638
|
+
interface Devtools {
|
|
2639
|
+
/** Handle a workflow event */
|
|
2640
|
+
handleEvent: (event: WorkflowEvent<unknown>) => void;
|
|
2641
|
+
/** Handle a decision event */
|
|
2642
|
+
handleDecisionEvent: (event: DecisionStartEvent | DecisionBranchEvent | DecisionEndEvent) => void;
|
|
2643
|
+
/** Get the current run */
|
|
2644
|
+
getCurrentRun: () => WorkflowRun | undefined;
|
|
2645
|
+
/** Get run history */
|
|
2646
|
+
getHistory: () => WorkflowRun[];
|
|
2647
|
+
/** Get a specific run by ID */
|
|
2648
|
+
getRun: (id: string) => WorkflowRun | undefined;
|
|
2649
|
+
/** Compare two runs */
|
|
2650
|
+
diff: (runId1: string, runId2: string) => RunDiff | undefined;
|
|
2651
|
+
/** Compare current run with a previous run */
|
|
2652
|
+
diffWithPrevious: () => RunDiff | undefined;
|
|
2653
|
+
/** Render current state */
|
|
2654
|
+
render: () => string;
|
|
2655
|
+
/** Render to a specific format */
|
|
2656
|
+
renderAs: (format: OutputFormat) => string;
|
|
2657
|
+
/** Render as Mermaid diagram */
|
|
2658
|
+
renderMermaid: () => string;
|
|
2659
|
+
/** Render as ASCII timeline */
|
|
2660
|
+
renderTimeline: () => string;
|
|
2661
|
+
/** Get timeline data for current run */
|
|
2662
|
+
getTimeline: () => TimelineEntry[];
|
|
2663
|
+
/** Clear all history */
|
|
2664
|
+
clearHistory: () => void;
|
|
2665
|
+
/** Reset current run */
|
|
2666
|
+
reset: () => void;
|
|
2667
|
+
/** Export run data as JSON */
|
|
2668
|
+
exportRun: (runId?: string) => string;
|
|
2669
|
+
/** Import run data from JSON */
|
|
2670
|
+
importRun: (json: string) => WorkflowRun;
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Create a devtools instance for workflow debugging.
|
|
2674
|
+
*
|
|
2675
|
+
* @example
|
|
2676
|
+
* ```typescript
|
|
2677
|
+
* const devtools = createDevtools({ workflowName: 'checkout' });
|
|
2678
|
+
*
|
|
2679
|
+
* const workflow = createWorkflow(deps, {
|
|
2680
|
+
* onEvent: devtools.handleEvent,
|
|
2681
|
+
* });
|
|
2682
|
+
*
|
|
2683
|
+
* await workflow(async (step) => { ... });
|
|
2684
|
+
*
|
|
2685
|
+
* // Visualize
|
|
2686
|
+
* console.log(devtools.render());
|
|
2687
|
+
* console.log(devtools.renderMermaid());
|
|
2688
|
+
*
|
|
2689
|
+
* // Compare with previous run
|
|
2690
|
+
* const diff = devtools.diffWithPrevious();
|
|
2691
|
+
* ```
|
|
2692
|
+
*/
|
|
2693
|
+
declare function createDevtools(options?: DevtoolsOptions): Devtools;
|
|
2694
|
+
/**
|
|
2695
|
+
* Render a run diff as a string.
|
|
2696
|
+
*/
|
|
2697
|
+
declare function renderDiff(diff: RunDiff): string;
|
|
2698
|
+
/**
|
|
2699
|
+
* Quick visualization helper for a single workflow run.
|
|
2700
|
+
*/
|
|
2701
|
+
declare function quickVisualize(workflowFn: (handleEvent: (event: WorkflowEvent<unknown>) => void) => Promise<unknown>, options?: DevtoolsOptions): Promise<string>;
|
|
2702
|
+
/**
|
|
2703
|
+
* Create an event handler that logs to console with pretty formatting.
|
|
2704
|
+
*/
|
|
2705
|
+
declare function createConsoleLogger(options?: {
|
|
2706
|
+
prefix?: string;
|
|
2707
|
+
colors?: boolean;
|
|
2708
|
+
}): (event: WorkflowEvent<unknown>) => void;
|
|
2709
|
+
|
|
2710
|
+
/**
|
|
2711
|
+
* @jagreehal/workflow/hitl
|
|
2712
|
+
*
|
|
2713
|
+
* Human-in-the-Loop Orchestration Helpers.
|
|
2714
|
+
* Provides pollers, webhook handlers, and resume injectors
|
|
2715
|
+
* for production-ready approval workflows.
|
|
2716
|
+
*/
|
|
2717
|
+
|
|
2718
|
+
/**
|
|
2719
|
+
* Options passed to the workflow factory by the HITL orchestrator.
|
|
2720
|
+
*/
|
|
2721
|
+
interface HITLWorkflowFactoryOptions {
|
|
2722
|
+
/** Resume state for replaying completed steps */
|
|
2723
|
+
resumeState?: ResumeState;
|
|
2724
|
+
/** Event handler for tracking workflow events (required for HITL) */
|
|
2725
|
+
onEvent: (event: WorkflowEvent<unknown>) => void;
|
|
2726
|
+
}
|
|
2727
|
+
/**
|
|
2728
|
+
* Approval status returned from the approval store.
|
|
2729
|
+
*/
|
|
2730
|
+
type ApprovalStatus<T = unknown> = {
|
|
2731
|
+
status: "pending";
|
|
2732
|
+
} | {
|
|
2733
|
+
status: "approved";
|
|
2734
|
+
value: T;
|
|
2735
|
+
approvedBy?: string;
|
|
2736
|
+
approvedAt?: number;
|
|
2737
|
+
} | {
|
|
2738
|
+
status: "rejected";
|
|
2739
|
+
reason: string;
|
|
2740
|
+
rejectedBy?: string;
|
|
2741
|
+
rejectedAt?: number;
|
|
2742
|
+
} | {
|
|
2743
|
+
status: "expired";
|
|
2744
|
+
expiredAt: number;
|
|
2745
|
+
};
|
|
2746
|
+
/**
|
|
2747
|
+
* Interface for approval storage backends.
|
|
2748
|
+
*/
|
|
2749
|
+
interface ApprovalStore {
|
|
2750
|
+
/**
|
|
2751
|
+
* Get the status of an approval.
|
|
2752
|
+
*/
|
|
2753
|
+
getApproval(key: string): Promise<ApprovalStatus>;
|
|
2754
|
+
/**
|
|
2755
|
+
* Create or update a pending approval request.
|
|
2756
|
+
*/
|
|
2757
|
+
createApproval(key: string, options?: {
|
|
2758
|
+
metadata?: Record<string, unknown>;
|
|
2759
|
+
expiresAt?: number;
|
|
2760
|
+
requestedBy?: string;
|
|
2761
|
+
}): Promise<void>;
|
|
2762
|
+
/**
|
|
2763
|
+
* Grant an approval.
|
|
2764
|
+
*/
|
|
2765
|
+
grantApproval<T>(key: string, value: T, options?: {
|
|
2766
|
+
approvedBy?: string;
|
|
2767
|
+
}): Promise<void>;
|
|
2768
|
+
/**
|
|
2769
|
+
* Reject an approval.
|
|
2770
|
+
*/
|
|
2771
|
+
rejectApproval(key: string, reason: string, options?: {
|
|
2772
|
+
rejectedBy?: string;
|
|
2773
|
+
}): Promise<void>;
|
|
2774
|
+
/**
|
|
2775
|
+
* Cancel a pending approval.
|
|
2776
|
+
*/
|
|
2777
|
+
cancelApproval(key: string): Promise<void>;
|
|
2778
|
+
/**
|
|
2779
|
+
* List all pending approvals.
|
|
2780
|
+
*/
|
|
2781
|
+
listPending(options?: {
|
|
2782
|
+
prefix?: string;
|
|
2783
|
+
}): Promise<string[]>;
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Saved workflow state for resumption.
|
|
2787
|
+
*/
|
|
2788
|
+
interface SavedWorkflowState {
|
|
2789
|
+
/** Unique identifier for this workflow run */
|
|
2790
|
+
runId: string;
|
|
2791
|
+
/** Workflow name/type */
|
|
2792
|
+
workflowName: string;
|
|
2793
|
+
/** Resume state with step results */
|
|
2794
|
+
resumeState: ResumeState;
|
|
2795
|
+
/** Pending approval keys */
|
|
2796
|
+
pendingApprovals: string[];
|
|
2797
|
+
/** Input that was passed to the workflow */
|
|
2798
|
+
input?: unknown;
|
|
2799
|
+
/** Custom metadata */
|
|
2800
|
+
metadata?: Record<string, unknown>;
|
|
2801
|
+
/** When the workflow was started */
|
|
2802
|
+
startedAt: number;
|
|
2803
|
+
/** When the state was last updated */
|
|
2804
|
+
updatedAt: number;
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Interface for workflow state storage.
|
|
2808
|
+
*/
|
|
2809
|
+
interface WorkflowStateStore {
|
|
2810
|
+
/**
|
|
2811
|
+
* Save workflow state.
|
|
2812
|
+
*/
|
|
2813
|
+
save(state: SavedWorkflowState): Promise<void>;
|
|
2814
|
+
/**
|
|
2815
|
+
* Load workflow state by run ID.
|
|
2816
|
+
*/
|
|
2817
|
+
load(runId: string): Promise<SavedWorkflowState | undefined>;
|
|
2818
|
+
/**
|
|
2819
|
+
* Delete workflow state.
|
|
2820
|
+
*/
|
|
2821
|
+
delete(runId: string): Promise<void>;
|
|
2822
|
+
/**
|
|
2823
|
+
* List all saved workflow states.
|
|
2824
|
+
*/
|
|
2825
|
+
list(options?: {
|
|
2826
|
+
workflowName?: string;
|
|
2827
|
+
hasPendingApprovals?: boolean;
|
|
2828
|
+
}): Promise<string[]>;
|
|
2829
|
+
/**
|
|
2830
|
+
* Find workflows waiting for a specific approval.
|
|
2831
|
+
*/
|
|
2832
|
+
findByPendingApproval(approvalKey: string): Promise<string[]>;
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Options for the HITL orchestrator.
|
|
2836
|
+
*/
|
|
2837
|
+
interface HITLOrchestratorOptions {
|
|
2838
|
+
/** Approval store for managing approval states */
|
|
2839
|
+
approvalStore: ApprovalStore;
|
|
2840
|
+
/** Workflow state store for persisting workflow state */
|
|
2841
|
+
workflowStateStore: WorkflowStateStore;
|
|
2842
|
+
/** Default expiration time for approvals (in milliseconds) */
|
|
2843
|
+
defaultExpirationMs?: number;
|
|
2844
|
+
/** Logger function */
|
|
2845
|
+
logger?: (message: string) => void;
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Result of executing a workflow that may pause for approval.
|
|
2849
|
+
* Uses unknown for error type since workflows add UnexpectedError to the union.
|
|
2850
|
+
*/
|
|
2851
|
+
type HITLExecutionResult<T, E> = {
|
|
2852
|
+
status: "completed";
|
|
2853
|
+
result: Result<T, E | unknown>;
|
|
2854
|
+
} | {
|
|
2855
|
+
status: "paused";
|
|
2856
|
+
runId: string;
|
|
2857
|
+
pendingApprovals: string[];
|
|
2858
|
+
reason?: string;
|
|
2859
|
+
} | {
|
|
2860
|
+
status: "resumed";
|
|
2861
|
+
runId: string;
|
|
2862
|
+
result: Result<T, E | unknown>;
|
|
2863
|
+
};
|
|
2864
|
+
/**
|
|
2865
|
+
* Poller configuration.
|
|
2866
|
+
*/
|
|
2867
|
+
interface PollerOptions {
|
|
2868
|
+
/** Polling interval in milliseconds */
|
|
2869
|
+
intervalMs: number;
|
|
2870
|
+
/** Maximum number of polls (undefined = unlimited) */
|
|
2871
|
+
maxPolls?: number;
|
|
2872
|
+
/** Timeout for the entire polling operation */
|
|
2873
|
+
timeoutMs?: number;
|
|
2874
|
+
/** Callback when polling starts */
|
|
2875
|
+
onPollStart?: () => void;
|
|
2876
|
+
/** Callback when a poll completes */
|
|
2877
|
+
onPollComplete?: (result: ApprovalStatus) => void;
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Create an in-memory approval store for development/testing.
|
|
2881
|
+
*/
|
|
2882
|
+
declare function createMemoryApprovalStore(): ApprovalStore;
|
|
2883
|
+
/**
|
|
2884
|
+
* Create an in-memory workflow state store for development/testing.
|
|
2885
|
+
*/
|
|
2886
|
+
declare function createMemoryWorkflowStateStore(): WorkflowStateStore;
|
|
2887
|
+
/**
|
|
2888
|
+
* HITL orchestrator interface.
|
|
2889
|
+
*/
|
|
2890
|
+
interface HITLOrchestrator {
|
|
2891
|
+
/**
|
|
2892
|
+
* Execute a workflow that may pause for approvals.
|
|
2893
|
+
* If the workflow pauses, state is automatically saved.
|
|
2894
|
+
*
|
|
2895
|
+
* The workflowFactory receives options including onEvent handler which MUST be
|
|
2896
|
+
* passed to createWorkflow for HITL tracking to work.
|
|
2897
|
+
*/
|
|
2898
|
+
execute<T, E, TInput>(workflowName: string, workflowFactory: (options: HITLWorkflowFactoryOptions) => Workflow<E, unknown>, workflowFn: (step: unknown, deps: unknown, input: TInput) => Promise<T>, input: TInput, options?: {
|
|
2899
|
+
runId?: string;
|
|
2900
|
+
metadata?: Record<string, unknown>;
|
|
2901
|
+
}): Promise<HITLExecutionResult<T, E>>;
|
|
2902
|
+
/**
|
|
2903
|
+
* Resume a paused workflow after approvals have been granted.
|
|
2904
|
+
*/
|
|
2905
|
+
resume<T, E, TInput>(runId: string, workflowFactory: (options: HITLWorkflowFactoryOptions) => Workflow<E, unknown>, workflowFn: (step: unknown, deps: unknown, input: TInput) => Promise<T>): Promise<HITLExecutionResult<T, E>>;
|
|
2906
|
+
/**
|
|
2907
|
+
* Grant an approval and automatically resume any waiting workflows.
|
|
2908
|
+
*/
|
|
2909
|
+
grantApproval<T>(approvalKey: string, value: T, options?: {
|
|
2910
|
+
approvedBy?: string;
|
|
2911
|
+
autoResume?: boolean;
|
|
2912
|
+
}): Promise<{
|
|
2913
|
+
grantedAt: number;
|
|
2914
|
+
resumedWorkflows: string[];
|
|
2915
|
+
}>;
|
|
2916
|
+
/**
|
|
2917
|
+
* Reject an approval.
|
|
2918
|
+
*/
|
|
2919
|
+
rejectApproval(approvalKey: string, reason: string, options?: {
|
|
2920
|
+
rejectedBy?: string;
|
|
2921
|
+
}): Promise<void>;
|
|
2922
|
+
/**
|
|
2923
|
+
* Poll for an approval to be granted.
|
|
2924
|
+
*/
|
|
2925
|
+
pollApproval<T>(approvalKey: string, options?: PollerOptions): Promise<ApprovalStatus<T>>;
|
|
2926
|
+
/**
|
|
2927
|
+
* Get the status of a workflow run.
|
|
2928
|
+
*/
|
|
2929
|
+
getWorkflowStatus(runId: string): Promise<SavedWorkflowState | undefined>;
|
|
2930
|
+
/**
|
|
2931
|
+
* List all pending workflows.
|
|
2932
|
+
*/
|
|
2933
|
+
listPendingWorkflows(workflowName?: string): Promise<string[]>;
|
|
2934
|
+
/**
|
|
2935
|
+
* Clean up completed workflows older than the specified age.
|
|
2936
|
+
*/
|
|
2937
|
+
cleanup(maxAgeMs: number): Promise<number>;
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Create a HITL orchestrator for managing approval workflows.
|
|
2941
|
+
*
|
|
2942
|
+
* @example
|
|
2943
|
+
* ```typescript
|
|
2944
|
+
* const orchestrator = createHITLOrchestrator({
|
|
2945
|
+
* approvalStore: createMemoryApprovalStore(),
|
|
2946
|
+
* workflowStateStore: createMemoryWorkflowStateStore(),
|
|
2947
|
+
* });
|
|
2948
|
+
*
|
|
2949
|
+
* // Execute workflow - IMPORTANT: pass onEvent to createWorkflow!
|
|
2950
|
+
* const result = await orchestrator.execute(
|
|
2951
|
+
* 'order-approval',
|
|
2952
|
+
* ({ resumeState, onEvent }) => createWorkflow(deps, { resumeState, onEvent }),
|
|
2953
|
+
* async (step, deps, input) => {
|
|
2954
|
+
* const order = await step(() => deps.createOrder(input));
|
|
2955
|
+
* const approval = await step(() => deps.requireApproval(order.id), { key: `approval:${order.id}` });
|
|
2956
|
+
* await step(() => deps.processOrder(order.id));
|
|
2957
|
+
* return { orderId: order.id, approvedBy: approval.approvedBy };
|
|
2958
|
+
* },
|
|
2959
|
+
* { items: [...], total: 100 }
|
|
2960
|
+
* );
|
|
2961
|
+
*
|
|
2962
|
+
* if (result.status === 'paused') {
|
|
2963
|
+
* console.log(`Workflow paused, waiting for: ${result.pendingApprovals}`);
|
|
2964
|
+
* }
|
|
2965
|
+
*
|
|
2966
|
+
* // Later, grant approval
|
|
2967
|
+
* await orchestrator.grantApproval(
|
|
2968
|
+
* `approval:${orderId}`,
|
|
2969
|
+
* { approvedBy: 'manager@example.com' },
|
|
2970
|
+
* { autoResume: true }
|
|
2971
|
+
* );
|
|
2972
|
+
* ```
|
|
2973
|
+
*/
|
|
2974
|
+
declare function createHITLOrchestrator(options: HITLOrchestratorOptions): HITLOrchestrator;
|
|
2975
|
+
/**
|
|
2976
|
+
* Approval webhook request body.
|
|
2977
|
+
*/
|
|
2978
|
+
interface ApprovalWebhookRequest {
|
|
2979
|
+
/** Approval key */
|
|
2980
|
+
key: string;
|
|
2981
|
+
/** Action: approve, reject, or cancel */
|
|
2982
|
+
action: "approve" | "reject" | "cancel";
|
|
2983
|
+
/** Value to inject (for approve) */
|
|
2984
|
+
value?: unknown;
|
|
2985
|
+
/** Reason (for reject) */
|
|
2986
|
+
reason?: string;
|
|
2987
|
+
/** Who performed this action */
|
|
2988
|
+
actorId?: string;
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Approval webhook response.
|
|
2992
|
+
*/
|
|
2993
|
+
interface ApprovalWebhookResponse {
|
|
2994
|
+
success: boolean;
|
|
2995
|
+
message: string;
|
|
2996
|
+
data?: {
|
|
2997
|
+
key: string;
|
|
2998
|
+
action: string;
|
|
2999
|
+
timestamp: number;
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Create a webhook handler for approval actions.
|
|
3004
|
+
*
|
|
3005
|
+
* @example
|
|
3006
|
+
* ```typescript
|
|
3007
|
+
* const handleApproval = createApprovalWebhookHandler(approvalStore);
|
|
3008
|
+
*
|
|
3009
|
+
* // Express
|
|
3010
|
+
* app.post('/api/approvals', async (req, res) => {
|
|
3011
|
+
* const result = await handleApproval(req.body);
|
|
3012
|
+
* res.json(result);
|
|
3013
|
+
* });
|
|
3014
|
+
* ```
|
|
3015
|
+
*/
|
|
3016
|
+
declare function createApprovalWebhookHandler(store: ApprovalStore): (request: ApprovalWebhookRequest) => Promise<ApprovalWebhookResponse>;
|
|
3017
|
+
/**
|
|
3018
|
+
* Create an approval checker function for use in approval steps.
|
|
3019
|
+
* This wraps the approval store with the standard checkApproval interface.
|
|
3020
|
+
*
|
|
3021
|
+
* @example
|
|
3022
|
+
* ```typescript
|
|
3023
|
+
* const checkApproval = createApprovalChecker(approvalStore);
|
|
3024
|
+
*
|
|
3025
|
+
* const requireManagerApproval = createApprovalStep<{ approvedBy: string }>({
|
|
3026
|
+
* key: 'manager-approval',
|
|
3027
|
+
* checkApproval: checkApproval('manager-approval'),
|
|
3028
|
+
* pendingReason: 'Waiting for manager approval',
|
|
3029
|
+
* });
|
|
3030
|
+
* ```
|
|
3031
|
+
*/
|
|
3032
|
+
declare function createApprovalChecker<T>(store: ApprovalStore): (key: string) => () => Promise<{
|
|
3033
|
+
status: "pending";
|
|
3034
|
+
} | {
|
|
3035
|
+
status: "approved";
|
|
3036
|
+
value: T;
|
|
3037
|
+
} | {
|
|
3038
|
+
status: "rejected";
|
|
3039
|
+
reason: string;
|
|
3040
|
+
}>;
|
|
3041
|
+
|
|
3042
|
+
/**
|
|
3043
|
+
* @jagreehal/workflow/testing
|
|
3044
|
+
*
|
|
3045
|
+
* Deterministic Workflow Testing Harness.
|
|
3046
|
+
* Provides tools for scripting step outcomes and asserting workflow behavior.
|
|
3047
|
+
*/
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* A scripted outcome for a step.
|
|
3051
|
+
*/
|
|
3052
|
+
type ScriptedOutcome<T = unknown, E = unknown> = {
|
|
3053
|
+
type: "ok";
|
|
3054
|
+
value: T;
|
|
3055
|
+
} | {
|
|
3056
|
+
type: "err";
|
|
3057
|
+
error: E;
|
|
3058
|
+
} | {
|
|
3059
|
+
type: "throw";
|
|
3060
|
+
error: unknown;
|
|
3061
|
+
};
|
|
3062
|
+
/**
|
|
3063
|
+
* Step invocation record.
|
|
3064
|
+
*/
|
|
3065
|
+
interface StepInvocation {
|
|
3066
|
+
/** Step name */
|
|
3067
|
+
name?: string;
|
|
3068
|
+
/** Step key */
|
|
3069
|
+
key?: string;
|
|
3070
|
+
/** Invocation order (0-indexed) */
|
|
3071
|
+
order: number;
|
|
3072
|
+
/** Timestamp when step was invoked */
|
|
3073
|
+
timestamp: number;
|
|
3074
|
+
/** Duration in milliseconds */
|
|
3075
|
+
durationMs?: number;
|
|
3076
|
+
/** Result of the step */
|
|
3077
|
+
result?: Result<unknown, unknown>;
|
|
3078
|
+
/** Whether the step was from cache */
|
|
3079
|
+
cached?: boolean;
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Assertion result.
|
|
3083
|
+
*/
|
|
3084
|
+
interface AssertionResult {
|
|
3085
|
+
passed: boolean;
|
|
3086
|
+
message: string;
|
|
3087
|
+
expected?: unknown;
|
|
3088
|
+
actual?: unknown;
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Test harness options.
|
|
3092
|
+
*/
|
|
3093
|
+
interface TestHarnessOptions {
|
|
3094
|
+
/** Whether to record step invocations */
|
|
3095
|
+
recordInvocations?: boolean;
|
|
3096
|
+
/** Custom clock for deterministic timing */
|
|
3097
|
+
clock?: () => number;
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Mock step function that returns scripted outcomes.
|
|
3101
|
+
*/
|
|
3102
|
+
type MockStep<E> = {
|
|
3103
|
+
/** Execute with a Result-returning operation */
|
|
3104
|
+
<T, StepE extends E>(operation: () => Result<T, StepE> | AsyncResult<T, StepE>, options?: StepOptions | string): Promise<T>;
|
|
3105
|
+
/** Execute with a direct Result */
|
|
3106
|
+
<T, StepE extends E>(result: Result<T, StepE> | AsyncResult<T, StepE>, options?: StepOptions | string): Promise<T>;
|
|
3107
|
+
/** step.try for catching throws */
|
|
3108
|
+
try: <T, Err extends E>(operation: () => T | Promise<T>, options: {
|
|
3109
|
+
error: Err;
|
|
3110
|
+
name?: string;
|
|
3111
|
+
key?: string;
|
|
3112
|
+
} | {
|
|
3113
|
+
onError: (cause: unknown) => Err;
|
|
3114
|
+
name?: string;
|
|
3115
|
+
key?: string;
|
|
3116
|
+
}) => Promise<T>;
|
|
3117
|
+
};
|
|
3118
|
+
/**
|
|
3119
|
+
* Workflow test harness interface.
|
|
3120
|
+
*/
|
|
3121
|
+
interface WorkflowHarness<E, Deps> {
|
|
3122
|
+
/**
|
|
3123
|
+
* Script step outcomes in order.
|
|
3124
|
+
* Each outcome will be returned for the corresponding step invocation.
|
|
3125
|
+
*/
|
|
3126
|
+
script(outcomes: ScriptedOutcome[]): void;
|
|
3127
|
+
/**
|
|
3128
|
+
* Script a specific step outcome by name or key.
|
|
3129
|
+
*/
|
|
3130
|
+
scriptStep(nameOrKey: string, outcome: ScriptedOutcome): void;
|
|
3131
|
+
/**
|
|
3132
|
+
* Run the workflow with scripted outcomes.
|
|
3133
|
+
*/
|
|
3134
|
+
run<T>(fn: (step: MockStep<E>, deps: Deps) => Promise<T>): Promise<Result<T, E | unknown>>;
|
|
3135
|
+
/**
|
|
3136
|
+
* Run the workflow with input.
|
|
3137
|
+
*/
|
|
3138
|
+
runWithInput<T, TInput>(input: TInput, fn: (step: MockStep<E>, deps: Deps, input: TInput) => Promise<T>): Promise<Result<T, E | unknown>>;
|
|
3139
|
+
/**
|
|
3140
|
+
* Get recorded step invocations.
|
|
3141
|
+
*/
|
|
3142
|
+
getInvocations(): StepInvocation[];
|
|
3143
|
+
/**
|
|
3144
|
+
* Assert that steps were invoked in order.
|
|
3145
|
+
*/
|
|
3146
|
+
assertSteps(expectedNames: string[]): AssertionResult;
|
|
3147
|
+
/**
|
|
3148
|
+
* Assert that a step was invoked with specific options.
|
|
3149
|
+
*/
|
|
3150
|
+
assertStepCalled(nameOrKey: string): AssertionResult;
|
|
3151
|
+
/**
|
|
3152
|
+
* Assert that a step was NOT invoked.
|
|
3153
|
+
*/
|
|
3154
|
+
assertStepNotCalled(nameOrKey: string): AssertionResult;
|
|
3155
|
+
/**
|
|
3156
|
+
* Assert the workflow result.
|
|
3157
|
+
*/
|
|
3158
|
+
assertResult<T>(result: Result<T, unknown>, expected: Result<T, unknown>): AssertionResult;
|
|
3159
|
+
/**
|
|
3160
|
+
* Clear all state for a new test.
|
|
3161
|
+
*/
|
|
3162
|
+
reset(): void;
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Create a test harness for a workflow.
|
|
3166
|
+
*
|
|
3167
|
+
* @example
|
|
3168
|
+
* ```typescript
|
|
3169
|
+
* const harness = createWorkflowHarness({ fetchUser, chargeCard });
|
|
3170
|
+
*
|
|
3171
|
+
* // Script step outcomes
|
|
3172
|
+
* harness.script([
|
|
3173
|
+
* { type: 'ok', value: { id: '1', name: 'Alice' } },
|
|
3174
|
+
* { type: 'ok', value: { transactionId: 'tx_123' } },
|
|
3175
|
+
* ]);
|
|
3176
|
+
*
|
|
3177
|
+
* // Run the workflow
|
|
3178
|
+
* const result = await harness.run(async (step, { fetchUser, chargeCard }) => {
|
|
3179
|
+
* const user = await step(() => fetchUser('1'), 'fetch-user');
|
|
3180
|
+
* const charge = await step(() => chargeCard(100), 'charge-card');
|
|
3181
|
+
* return { user, charge };
|
|
3182
|
+
* });
|
|
3183
|
+
*
|
|
3184
|
+
* // Assert
|
|
3185
|
+
* expect(result.ok).toBe(true);
|
|
3186
|
+
* harness.assertSteps(['fetch-user', 'charge-card']);
|
|
3187
|
+
* ```
|
|
3188
|
+
*/
|
|
3189
|
+
declare function createWorkflowHarness<Deps extends Record<string, AnyResultFn$1>>(deps: Deps, options?: TestHarnessOptions): WorkflowHarness<ErrorsOfDeps$1<Deps>, Deps>;
|
|
3190
|
+
/**
|
|
3191
|
+
* Create a mock Result-returning function.
|
|
3192
|
+
*
|
|
3193
|
+
* @example
|
|
3194
|
+
* ```typescript
|
|
3195
|
+
* const fetchUser = createMockFn<User, 'NOT_FOUND'>();
|
|
3196
|
+
*
|
|
3197
|
+
* fetchUser.returns(ok({ id: '1', name: 'Alice' }));
|
|
3198
|
+
* // or
|
|
3199
|
+
* fetchUser.returnsOnce(ok({ id: '1', name: 'Alice' }));
|
|
3200
|
+
* fetchUser.returnsOnce(err('NOT_FOUND'));
|
|
3201
|
+
* ```
|
|
3202
|
+
*/
|
|
3203
|
+
declare function createMockFn<T, E>(): MockFunction<T, E>;
|
|
3204
|
+
/**
|
|
3205
|
+
* Mock function interface.
|
|
3206
|
+
*/
|
|
3207
|
+
interface MockFunction<T, E> {
|
|
3208
|
+
(...args: unknown[]): AsyncResult<T, E>;
|
|
3209
|
+
/** Set the default return value */
|
|
3210
|
+
returns(result: Result<T, E>): MockFunction<T, E>;
|
|
3211
|
+
/** Queue a return value for the next call */
|
|
3212
|
+
returnsOnce(result: Result<T, E>): MockFunction<T, E>;
|
|
3213
|
+
/** Get all call arguments */
|
|
3214
|
+
getCalls(): unknown[][];
|
|
3215
|
+
/** Get the number of times the function was called */
|
|
3216
|
+
getCallCount(): number;
|
|
3217
|
+
/** Reset the mock */
|
|
3218
|
+
reset(): void;
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Workflow snapshot for comparison.
|
|
3222
|
+
*/
|
|
3223
|
+
interface WorkflowSnapshot {
|
|
3224
|
+
/** Step invocations */
|
|
3225
|
+
invocations: StepInvocation[];
|
|
3226
|
+
/** Final result */
|
|
3227
|
+
result: Result<unknown, unknown>;
|
|
3228
|
+
/** Events emitted */
|
|
3229
|
+
events?: WorkflowEvent<unknown>[];
|
|
3230
|
+
/** Total duration */
|
|
3231
|
+
durationMs?: number;
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Create a snapshot of a workflow execution.
|
|
3235
|
+
*/
|
|
3236
|
+
declare function createSnapshot(invocations: StepInvocation[], result: Result<unknown, unknown>, events?: WorkflowEvent<unknown>[]): WorkflowSnapshot;
|
|
3237
|
+
/**
|
|
3238
|
+
* Compare two workflow snapshots.
|
|
3239
|
+
*/
|
|
3240
|
+
declare function compareSnapshots(snapshot1: WorkflowSnapshot, snapshot2: WorkflowSnapshot): {
|
|
3241
|
+
equal: boolean;
|
|
3242
|
+
differences: string[];
|
|
3243
|
+
};
|
|
3244
|
+
/**
|
|
3245
|
+
* Create a deterministic clock for testing.
|
|
3246
|
+
*/
|
|
3247
|
+
declare function createTestClock(startTime?: number): {
|
|
3248
|
+
now: () => number;
|
|
3249
|
+
advance: (ms: number) => void;
|
|
3250
|
+
set: (time: number) => void;
|
|
3251
|
+
reset: () => void;
|
|
3252
|
+
};
|
|
3253
|
+
/**
|
|
3254
|
+
* Helper to create ok outcomes.
|
|
3255
|
+
*/
|
|
3256
|
+
declare function okOutcome<T>(value: T): ScriptedOutcome<T, never>;
|
|
3257
|
+
/**
|
|
3258
|
+
* Helper to create err outcomes.
|
|
3259
|
+
*/
|
|
3260
|
+
declare function errOutcome<E>(error: E): ScriptedOutcome<never, E>;
|
|
3261
|
+
/**
|
|
3262
|
+
* Helper to create throw outcomes.
|
|
3263
|
+
*/
|
|
3264
|
+
declare function throwOutcome(error: unknown): ScriptedOutcome<never, never>;
|
|
3265
|
+
|
|
3266
|
+
export { AnyResultFn$1 as AnyResultFn, type ApprovalStatus, type ApprovalStore, type ApprovalWebhookRequest, type ApprovalWebhookResponse, type AssertionResult, AsyncResult, type AutotelAdapter, type AutotelAdapterConfig, type AutotelMetrics, type AutotelTraceFn, type CircuitBreaker, type CircuitBreakerConfig, type CircuitBreakerStats, CircuitOpenError, type CircuitState, type CombinedLimiterConfig, type CompensationAction, type ConcurrencyLimiter, type ConcurrencyLimiterConfig, type ConcurrencyLimiterStats, type ConditionalContext, type ConditionalOptions, type Devtools, type DevtoolsOptions, type ErrorMapping, type ErrorResponseBody, ErrorsOfDeps$1 as ErrorsOfDeps, type EventHandler, type EventMessage, type EventProcessingResult, type EventTriggerConfig, type ExpressLikeRequest, type ExpressLikeResponse, type FileCacheOptions, type FileSystemInterface, type HITLExecutionResult, type HITLOrchestrator, type HITLOrchestratorOptions, type HITLWorkflowFactoryOptions, type KVCacheOptions, type KeyValueStore, type MemoryCacheOptions, type MigrationError, type MigrationFn, type Migrations, type MockFunction, type MockStep, type NamedPolicy, type Policy, type PolicyFactory, type PolicyRegistry, type PollerOptions, type QueueFullError, type RateLimitExceededError, type RateLimiter, type RateLimiterConfig, type RateLimiterStats, Result, ResumeState, ResumeStateEntry, RetryOptions, type RunDiff, type SagaCompensationError, type SagaContext, type SagaEvent, type SagaResult, type SagaStepOptions, type SagaWorkflowOptions, type SavedWorkflowState, type ScriptedOutcome, type SerializedCause, type SerializedEntry, type SerializedMeta, type SerializedResult, type SerializedState, type SimpleHandlerConfig, type StatePersistence, StepCache, type StepDiff, type StepInvocation, StepOptions, type StepOptionsBuilder, type TestHarnessOptions, type TimelineEntry, TimeoutOptions, UnexpectedError, type ValidationError, type ValidationResult, type Version, type VersionIncompatibleError, type VersionedState, type VersionedWorkflowConfig, type WebhookHandler, type WebhookHandlerConfig, type WebhookRequest, type WebhookResponse, type WithPoliciesOptions, Workflow, WorkflowEvent, type WorkflowHarness, type WorkflowRun, type WorkflowSnapshot, type WorkflowStateStore, WorkflowStrict, circuitBreakerPresets, compareSnapshots, composeMigrations, composeValidators, conditionalPolicy, createApprovalChecker, createApprovalWebhookHandler, createAutotelAdapter, createAutotelEventHandler, createCircuitBreaker, createCombinedLimiter, createConcurrencyLimiter, createConditionalHelpers, createConsoleLogger, createDevtools, createEventHandler, createExpressHandler, createFileCache, createHITLOrchestrator, createHydratingCache, createKVCache, createKeyRemoveMigration, createKeyRenameMigration, createMemoryApprovalStore, createMemoryCache, createMemoryWorkflowStateStore, createMockFn, createPolicyApplier, createPolicyBundle, createPolicyRegistry, createRateLimiter, createResultMapper, createSagaWorkflow, createSimpleHandler, createSnapshot, createStatePersistence, createTestClock, createValueTransformMigration, createVersionedState, createVersionedStateLoader, createWebhookHandler, createWorkflowHarness, defaultUnexpectedErrorMapper, defaultValidationErrorMapper, deserializeCause, deserializeEntry, deserializeMeta, deserializeResult, deserializeState, envPolicy, errOutcome, isCircuitOpenError, isMigrationError, isQueueFullError, isRateLimitExceededError, isSagaCompensationError, isValidationError, isVersionIncompatibleError, mergePolicies, migrateState, okOutcome, parseState, parseVersionedState, quickVisualize, rateLimiterPresets, renderDiff, requireFields, retryPolicies, retryPolicy, runSaga, sendWebhookResponse, serializeCause, serializeEntry, serializeMeta, serializeResult, serializeState, servicePolicies, stepOptions, stringifyState, stringifyVersionedState, throwOutcome, timeoutPolicies, timeoutPolicy, toWebhookRequest, unless, unlessOr, validationError, when, whenOr, withAutotelTracing, withPolicies, withPolicy };
|