ai-workflows 2.1.3 → 2.4.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +14 -1
- package/README.md +2 -0
- package/dist/barrier.d.ts +6 -0
- package/dist/barrier.d.ts.map +1 -1
- package/dist/barrier.js +45 -7
- package/dist/barrier.js.map +1 -1
- package/dist/cascade-context.d.ts.map +1 -1
- package/dist/cascade-context.js +25 -25
- package/dist/cascade-context.js.map +1 -1
- package/dist/cascade-executor.d.ts.map +1 -1
- package/dist/cascade-executor.js +1 -1
- package/dist/cascade-executor.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -7
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- package/dist/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.js.map +1 -0
- package/dist/graph/topological-sort.d.ts.map +1 -1
- package/dist/graph/topological-sort.js +5 -5
- package/dist/graph/topological-sort.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +3 -3
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +25 -0
- package/dist/timer-registry.d.ts.map +1 -1
- package/dist/timer-registry.js +42 -8
- package/dist/timer-registry.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +132 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +30 -13
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +48 -7
- package/src/cascade-context.ts +36 -29
- package/src/cascade-executor.ts +3 -2
- package/src/context.ts +41 -12
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/graph/topological-sort.ts +6 -8
- package/src/index.ts +69 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +8 -9
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +44 -10
- package/src/types.ts +32 -17
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +188 -351
- package/test/barrier-join.test.ts +32 -24
- package/test/cascade-executor.test.ts +9 -16
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +30 -21
- package/test/send-race-conditions.test.ts +30 -40
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -169
- package/LICENSE +0 -21
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- package/vitest.config.js +0 -7
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowBuilder DSL - Fluent API for building durable workflows
|
|
3
|
+
*
|
|
4
|
+
* Provides a declarative DSL for workflow definition with:
|
|
5
|
+
* - Sequential steps with .step()
|
|
6
|
+
* - Parallel execution with .parallel()
|
|
7
|
+
* - Conditional branching with .when().then().else()
|
|
8
|
+
* - Loops with .loop() and .forEach()
|
|
9
|
+
* - Error handling with .onError()
|
|
10
|
+
* - Timeouts with .timeout()
|
|
11
|
+
* - Retries with .retry()
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { workflow } from 'ai-workflows'
|
|
16
|
+
*
|
|
17
|
+
* const orderWorkflow = workflow('order-process')
|
|
18
|
+
* .step('validate', async (input) => ({ valid: true, ...input }))
|
|
19
|
+
* .when(ctx => ctx.result.valid)
|
|
20
|
+
* .then(
|
|
21
|
+
* workflow('charge-flow')
|
|
22
|
+
* .step('charge', async () => ({ charged: true }))
|
|
23
|
+
* )
|
|
24
|
+
* .step('fulfill', fulfillOrder)
|
|
25
|
+
* .timeout(5000)
|
|
26
|
+
* .retry({ attempts: 3, backoff: 'exponential' })
|
|
27
|
+
* .build()
|
|
28
|
+
*
|
|
29
|
+
* const result = await orderWorkflow.execute({ orderId: '123' })
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @packageDocumentation
|
|
33
|
+
*/
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Helper Functions
|
|
36
|
+
// ============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Parse duration string to milliseconds
|
|
39
|
+
*/
|
|
40
|
+
function parseDuration(duration) {
|
|
41
|
+
if (typeof duration === 'number')
|
|
42
|
+
return duration;
|
|
43
|
+
const match = duration.match(/^(\d+)(ms|s|m|h)?$/);
|
|
44
|
+
if (!match || match[1] === undefined)
|
|
45
|
+
return parseInt(duration, 10);
|
|
46
|
+
const value = parseInt(match[1], 10);
|
|
47
|
+
const unit = match[2] || 'ms';
|
|
48
|
+
switch (unit) {
|
|
49
|
+
case 's':
|
|
50
|
+
return value * 1000;
|
|
51
|
+
case 'm':
|
|
52
|
+
return value * 60 * 1000;
|
|
53
|
+
case 'h':
|
|
54
|
+
return value * 60 * 60 * 1000;
|
|
55
|
+
default:
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Calculate backoff delay
|
|
61
|
+
*/
|
|
62
|
+
function calculateBackoff(attempt, config) {
|
|
63
|
+
const baseDelay = config.delay || 100;
|
|
64
|
+
let delay;
|
|
65
|
+
switch (config.backoff) {
|
|
66
|
+
case 'linear':
|
|
67
|
+
delay = baseDelay * attempt;
|
|
68
|
+
break;
|
|
69
|
+
case 'exponential':
|
|
70
|
+
delay = baseDelay * Math.pow(2, attempt - 1);
|
|
71
|
+
break;
|
|
72
|
+
case 'constant':
|
|
73
|
+
default:
|
|
74
|
+
delay = baseDelay;
|
|
75
|
+
}
|
|
76
|
+
// Apply max delay cap
|
|
77
|
+
if (config.maxDelay) {
|
|
78
|
+
delay = Math.min(delay, config.maxDelay);
|
|
79
|
+
}
|
|
80
|
+
// Apply jitter
|
|
81
|
+
if (config.jitter) {
|
|
82
|
+
delay = delay * (0.5 + Math.random());
|
|
83
|
+
}
|
|
84
|
+
return delay;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Execute with timeout
|
|
88
|
+
*/
|
|
89
|
+
async function withTimeout(promise, ms, stepName) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
reject(new Error(`Timeout: step "${stepName || 'unknown'}" exceeded ${ms}ms`));
|
|
93
|
+
}, ms);
|
|
94
|
+
promise
|
|
95
|
+
.then((result) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve(result);
|
|
98
|
+
})
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
reject(error);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Execute with retry
|
|
107
|
+
*/
|
|
108
|
+
async function withRetry(fn, config, stepName) {
|
|
109
|
+
let lastError;
|
|
110
|
+
for (let attempt = 1; attempt <= config.attempts; attempt++) {
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
lastError = error;
|
|
116
|
+
// Check if we should retry
|
|
117
|
+
if (config.retryIf && !config.retryIf(lastError, attempt)) {
|
|
118
|
+
throw lastError;
|
|
119
|
+
}
|
|
120
|
+
// Don't wait after last attempt
|
|
121
|
+
if (attempt < config.attempts) {
|
|
122
|
+
const delay = calculateBackoff(attempt, config);
|
|
123
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw lastError;
|
|
128
|
+
}
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// WorkflowBuilder Implementation
|
|
131
|
+
// ============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* WorkflowBuilder - Fluent DSL for building durable workflows
|
|
134
|
+
*/
|
|
135
|
+
export class WorkflowBuilder {
|
|
136
|
+
/** Workflow name */
|
|
137
|
+
name;
|
|
138
|
+
_steps = [];
|
|
139
|
+
_stepNames = new Set();
|
|
140
|
+
_defaultRetryConfig;
|
|
141
|
+
_workflowErrorHandler;
|
|
142
|
+
_workflowTimeout;
|
|
143
|
+
// Track if the last operation was a configuration (timeout/retry) without step
|
|
144
|
+
_lastOpWasConfig = false;
|
|
145
|
+
// Track which step index the last config applied to (-1 for workflow level)
|
|
146
|
+
_lastConfigStepIndex = -1;
|
|
147
|
+
// Track the step index that was most recently directly configured (for step-level config)
|
|
148
|
+
_lastDirectlyConfiguredStep = -1;
|
|
149
|
+
constructor(name) {
|
|
150
|
+
if (!name || name.trim() === '') {
|
|
151
|
+
throw new Error('Workflow name is required');
|
|
152
|
+
}
|
|
153
|
+
this.name = name;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Add a sequential step
|
|
157
|
+
*/
|
|
158
|
+
step(name, fn) {
|
|
159
|
+
// Defer duplicate check to build() for immutability
|
|
160
|
+
this._steps.push({
|
|
161
|
+
type: 'step',
|
|
162
|
+
name,
|
|
163
|
+
fn: fn,
|
|
164
|
+
});
|
|
165
|
+
this._lastOpWasConfig = false;
|
|
166
|
+
this._lastConfigStepIndex = -1;
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Add parallel steps
|
|
171
|
+
*/
|
|
172
|
+
parallel(steps) {
|
|
173
|
+
this._steps.push({
|
|
174
|
+
type: 'parallel',
|
|
175
|
+
parallelSteps: steps,
|
|
176
|
+
});
|
|
177
|
+
this._lastOpWasConfig = false;
|
|
178
|
+
this._lastConfigStepIndex = -1;
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Add conditional branching
|
|
183
|
+
*/
|
|
184
|
+
when(condition) {
|
|
185
|
+
const self = this;
|
|
186
|
+
return {
|
|
187
|
+
then(branch) {
|
|
188
|
+
const thenBranch = branch instanceof WorkflowBuilder
|
|
189
|
+
? branch
|
|
190
|
+
: workflow('inline-then').step('inline', branch);
|
|
191
|
+
// Create step definition but don't add it yet
|
|
192
|
+
const stepDef = {
|
|
193
|
+
type: 'conditional',
|
|
194
|
+
conditional: {
|
|
195
|
+
condition,
|
|
196
|
+
thenBranch: thenBranch,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
else(elseBranch) {
|
|
201
|
+
const resolvedElseBranch = elseBranch instanceof WorkflowBuilder
|
|
202
|
+
? elseBranch
|
|
203
|
+
: workflow('inline-else').step('inline', elseBranch);
|
|
204
|
+
stepDef.conditional.elseBranch = resolvedElseBranch;
|
|
205
|
+
self._steps.push(stepDef);
|
|
206
|
+
return self;
|
|
207
|
+
},
|
|
208
|
+
step(name, fn) {
|
|
209
|
+
self._steps.push(stepDef);
|
|
210
|
+
return self.step(name, fn);
|
|
211
|
+
},
|
|
212
|
+
parallel(steps) {
|
|
213
|
+
self._steps.push(stepDef);
|
|
214
|
+
return self.parallel(steps);
|
|
215
|
+
},
|
|
216
|
+
when(cond) {
|
|
217
|
+
self._steps.push(stepDef);
|
|
218
|
+
return self.when(cond);
|
|
219
|
+
},
|
|
220
|
+
loop(cond, body, options) {
|
|
221
|
+
self._steps.push(stepDef);
|
|
222
|
+
return self.loop(cond, body, options);
|
|
223
|
+
},
|
|
224
|
+
forEach(itemsSelector, body, options) {
|
|
225
|
+
self._steps.push(stepDef);
|
|
226
|
+
return self.forEach(itemsSelector, body, options);
|
|
227
|
+
},
|
|
228
|
+
onError(handler) {
|
|
229
|
+
self._steps.push(stepDef);
|
|
230
|
+
return self.onError(handler);
|
|
231
|
+
},
|
|
232
|
+
timeout(ms) {
|
|
233
|
+
self._steps.push(stepDef);
|
|
234
|
+
return self.timeout(ms);
|
|
235
|
+
},
|
|
236
|
+
retry(config) {
|
|
237
|
+
self._steps.push(stepDef);
|
|
238
|
+
return self.retry(config);
|
|
239
|
+
},
|
|
240
|
+
build() {
|
|
241
|
+
self._steps.push(stepDef);
|
|
242
|
+
return self.build();
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Add a loop
|
|
250
|
+
*/
|
|
251
|
+
loop(condition, body, options) {
|
|
252
|
+
this._steps.push({
|
|
253
|
+
type: 'loop',
|
|
254
|
+
loop: {
|
|
255
|
+
condition,
|
|
256
|
+
body,
|
|
257
|
+
...(options !== undefined && { options }),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
this._lastOpWasConfig = false;
|
|
261
|
+
this._lastConfigStepIndex = -1;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Add forEach iteration
|
|
266
|
+
*/
|
|
267
|
+
forEach(itemsSelector, body, options) {
|
|
268
|
+
this._steps.push({
|
|
269
|
+
type: 'forEach',
|
|
270
|
+
forEach: {
|
|
271
|
+
itemsSelector,
|
|
272
|
+
body,
|
|
273
|
+
...(options !== undefined && { options }),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
this._lastOpWasConfig = false;
|
|
277
|
+
this._lastConfigStepIndex = -1;
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Set error handler for the most recent step or workflow
|
|
282
|
+
*
|
|
283
|
+
* Rules:
|
|
284
|
+
* - If no steps exist, applies to workflow
|
|
285
|
+
* - If last config (timeout/retry) was workflow-level, this is workflow-level too
|
|
286
|
+
* - If last config was step-level, this applies to that same step
|
|
287
|
+
* - If multiple steps since last config, this is workflow-level
|
|
288
|
+
*/
|
|
289
|
+
onError(handler) {
|
|
290
|
+
const lastStepIndex = this._steps.length - 1;
|
|
291
|
+
// Determine if this should be workflow-level or step-level
|
|
292
|
+
let isWorkflowLevel = false;
|
|
293
|
+
if (this._steps.length === 0) {
|
|
294
|
+
isWorkflowLevel = true;
|
|
295
|
+
}
|
|
296
|
+
else if (this._lastOpWasConfig && this._lastConfigStepIndex === -1) {
|
|
297
|
+
// Last config was workflow-level (e.g., workflow timeout)
|
|
298
|
+
isWorkflowLevel = true;
|
|
299
|
+
}
|
|
300
|
+
else if (this._lastConfigStepIndex === lastStepIndex) {
|
|
301
|
+
// Last config was for the most recent step - stay step-level
|
|
302
|
+
isWorkflowLevel = false;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Check if multiple steps since last config
|
|
306
|
+
const unconfiguredStepsCount = lastStepIndex - this._lastDirectlyConfiguredStep;
|
|
307
|
+
if (unconfiguredStepsCount > 1) {
|
|
308
|
+
isWorkflowLevel = true;
|
|
309
|
+
}
|
|
310
|
+
else if (unconfiguredStepsCount === 1) {
|
|
311
|
+
// One unconfigured step - apply to that step
|
|
312
|
+
isWorkflowLevel = false;
|
|
313
|
+
this._lastDirectlyConfiguredStep = lastStepIndex;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
isWorkflowLevel = false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (isWorkflowLevel) {
|
|
320
|
+
// Workflow-level error handler
|
|
321
|
+
if (this._workflowErrorHandler) {
|
|
322
|
+
// Chain error handlers
|
|
323
|
+
const previousHandler = this._workflowErrorHandler;
|
|
324
|
+
this._workflowErrorHandler = async (error, ctx) => {
|
|
325
|
+
try {
|
|
326
|
+
return await previousHandler(error, ctx);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
return await handler(e, ctx);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
this._workflowErrorHandler = handler;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Step-level error handler - apply to the step that was just configured
|
|
339
|
+
const targetStepIndex = this._lastConfigStepIndex >= 0 ? this._lastConfigStepIndex : lastStepIndex;
|
|
340
|
+
const targetStep = this._steps[targetStepIndex];
|
|
341
|
+
if (targetStep) {
|
|
342
|
+
if (targetStep.errorHandler) {
|
|
343
|
+
// Chain error handlers
|
|
344
|
+
const previousHandler = targetStep.errorHandler;
|
|
345
|
+
targetStep.errorHandler = async (error, ctx) => {
|
|
346
|
+
try {
|
|
347
|
+
return await previousHandler(error, ctx);
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
return await handler(e, ctx);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
targetStep.errorHandler = handler;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Set timeout for the most recent step or workflow
|
|
363
|
+
*
|
|
364
|
+
* Rules:
|
|
365
|
+
* - If no steps exist, applies to workflow
|
|
366
|
+
* - If called immediately after a step that was just configured (same step), applies to that step
|
|
367
|
+
* - If called after a step that hasn't been configured yet (first config after that step), applies to that step
|
|
368
|
+
* - If multiple steps were added since last config, this becomes workflow-level
|
|
369
|
+
*/
|
|
370
|
+
timeout(ms) {
|
|
371
|
+
const timeout = parseDuration(ms);
|
|
372
|
+
const lastStepIndex = this._steps.length - 1;
|
|
373
|
+
if (this._steps.length === 0) {
|
|
374
|
+
// No steps - workflow level
|
|
375
|
+
this._workflowTimeout = timeout;
|
|
376
|
+
this._lastConfigStepIndex = -1;
|
|
377
|
+
}
|
|
378
|
+
else if (this._lastDirectlyConfiguredStep === lastStepIndex) {
|
|
379
|
+
// Same step was already configured - still step level for this step
|
|
380
|
+
const lastStep = this._steps[lastStepIndex];
|
|
381
|
+
if (lastStep) {
|
|
382
|
+
lastStep.timeout = timeout;
|
|
383
|
+
}
|
|
384
|
+
this._lastConfigStepIndex = lastStepIndex;
|
|
385
|
+
}
|
|
386
|
+
else if (this._lastDirectlyConfiguredStep === lastStepIndex - 1 ||
|
|
387
|
+
this._lastDirectlyConfiguredStep === -1) {
|
|
388
|
+
// Previous step was configured, or no step configured yet
|
|
389
|
+
// Check if there's only one unconfigured step (step-level) or multiple (workflow-level)
|
|
390
|
+
const unconfiguredStepsCount = lastStepIndex - this._lastDirectlyConfiguredStep;
|
|
391
|
+
if (unconfiguredStepsCount === 1) {
|
|
392
|
+
// Only one step since last config - apply to that step
|
|
393
|
+
const lastStep = this._steps[lastStepIndex];
|
|
394
|
+
if (lastStep) {
|
|
395
|
+
lastStep.timeout = timeout;
|
|
396
|
+
}
|
|
397
|
+
this._lastConfigStepIndex = lastStepIndex;
|
|
398
|
+
this._lastDirectlyConfiguredStep = lastStepIndex;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// Multiple steps since last config - apply to workflow
|
|
402
|
+
this._workflowTimeout = timeout;
|
|
403
|
+
this._lastConfigStepIndex = -1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// More than one step was added since last config - workflow level
|
|
408
|
+
this._workflowTimeout = timeout;
|
|
409
|
+
this._lastConfigStepIndex = -1;
|
|
410
|
+
}
|
|
411
|
+
this._lastOpWasConfig = true;
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Set retry configuration for the most recent step or workflow
|
|
416
|
+
*
|
|
417
|
+
* When called immediately after a step, applies to that step.
|
|
418
|
+
* When no steps exist, applies as default for all steps.
|
|
419
|
+
*/
|
|
420
|
+
retry(config) {
|
|
421
|
+
if (this._steps.length > 0 && !this._lastOpWasConfig) {
|
|
422
|
+
// Apply to last step
|
|
423
|
+
const lastStep = this._steps[this._steps.length - 1];
|
|
424
|
+
if (lastStep) {
|
|
425
|
+
lastStep.retry = config;
|
|
426
|
+
}
|
|
427
|
+
this._lastConfigStepIndex = this._steps.length - 1;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// Apply as workflow default
|
|
431
|
+
this._defaultRetryConfig = config;
|
|
432
|
+
this._lastConfigStepIndex = -1;
|
|
433
|
+
}
|
|
434
|
+
this._lastOpWasConfig = true;
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Build the workflow definition
|
|
439
|
+
*/
|
|
440
|
+
build() {
|
|
441
|
+
// Check for duplicate step names
|
|
442
|
+
const names = new Set();
|
|
443
|
+
for (const step of this._steps) {
|
|
444
|
+
if (step.type === 'step' && step.name) {
|
|
445
|
+
if (names.has(step.name)) {
|
|
446
|
+
throw new Error(`Duplicate step name: "${step.name}"`);
|
|
447
|
+
}
|
|
448
|
+
names.add(step.name);
|
|
449
|
+
}
|
|
450
|
+
if (step.type === 'parallel' && step.parallelSteps) {
|
|
451
|
+
for (const ps of step.parallelSteps) {
|
|
452
|
+
if (names.has(ps.name)) {
|
|
453
|
+
throw new Error(`Duplicate step name: "${ps.name}"`);
|
|
454
|
+
}
|
|
455
|
+
names.add(ps.name);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Create immutable copies
|
|
460
|
+
const steps = this._steps.map((s) => ({ ...s }));
|
|
461
|
+
const defaultRetryConfig = this._defaultRetryConfig;
|
|
462
|
+
const workflowErrorHandler = this._workflowErrorHandler;
|
|
463
|
+
const workflowTimeout = this._workflowTimeout;
|
|
464
|
+
const workflowName = this.name;
|
|
465
|
+
return {
|
|
466
|
+
name: workflowName,
|
|
467
|
+
steps: Object.freeze(steps),
|
|
468
|
+
...(defaultRetryConfig !== undefined && { defaultRetryConfig }),
|
|
469
|
+
execute: async (input) => {
|
|
470
|
+
return executeWorkflow(steps, input, defaultRetryConfig, workflowErrorHandler, workflowTimeout);
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// Workflow Execution
|
|
477
|
+
// ============================================================================
|
|
478
|
+
/**
|
|
479
|
+
* Execute a workflow
|
|
480
|
+
*/
|
|
481
|
+
async function executeWorkflow(steps, input, defaultRetryConfig, workflowErrorHandler, workflowTimeout) {
|
|
482
|
+
let result = {};
|
|
483
|
+
const startTime = Date.now();
|
|
484
|
+
const createContext = (currentStep) => ({
|
|
485
|
+
input,
|
|
486
|
+
result: { ...result },
|
|
487
|
+
...(currentStep !== undefined && { currentStep }),
|
|
488
|
+
retry: async () => {
|
|
489
|
+
throw new Error('retry() can only be called from an error handler');
|
|
490
|
+
},
|
|
491
|
+
skip: (skipResult) => ({ __skip: true, result: skipResult }),
|
|
492
|
+
abort: (reason) => {
|
|
493
|
+
throw new Error(reason || 'Workflow aborted');
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
// Helper to check workflow timeout
|
|
497
|
+
const checkWorkflowTimeout = () => {
|
|
498
|
+
if (workflowTimeout && Date.now() - startTime > workflowTimeout) {
|
|
499
|
+
throw new Error('Timeout: workflow exceeded timeout');
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const executeWithWorkflowTimeout = async () => {
|
|
503
|
+
for (const step of steps) {
|
|
504
|
+
// Check workflow timeout before each step
|
|
505
|
+
checkWorkflowTimeout();
|
|
506
|
+
try {
|
|
507
|
+
result = await executeStep(step, input, result, createContext, defaultRetryConfig, workflowTimeout ? workflowTimeout - (Date.now() - startTime) : undefined);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
// Try step-level error handler first
|
|
511
|
+
if (step.errorHandler) {
|
|
512
|
+
let currentError = error;
|
|
513
|
+
let retryRequested = false;
|
|
514
|
+
let retrySucceeded = false;
|
|
515
|
+
let retryResult = null;
|
|
516
|
+
const maxRetryAttempts = 100; // Safety limit
|
|
517
|
+
for (let retryAttempt = 0; retryAttempt < maxRetryAttempts; retryAttempt++) {
|
|
518
|
+
retryRequested = false;
|
|
519
|
+
// Create context with retry support
|
|
520
|
+
const errorCtx = {
|
|
521
|
+
input,
|
|
522
|
+
result: { ...result },
|
|
523
|
+
...(step.name !== undefined && { currentStep: step.name }),
|
|
524
|
+
retry: async () => {
|
|
525
|
+
retryRequested = true;
|
|
526
|
+
// Re-execute just the step function directly
|
|
527
|
+
const stepInput = Object.keys(result).length > 0 ? result : input;
|
|
528
|
+
const stepResult = await step.fn(stepInput, createContext(step.name));
|
|
529
|
+
retryResult = { ...result, ...stepResult };
|
|
530
|
+
retrySucceeded = true;
|
|
531
|
+
return retryResult;
|
|
532
|
+
},
|
|
533
|
+
skip: (skipResult) => ({ __skip: true, result: skipResult }),
|
|
534
|
+
abort: (reason) => {
|
|
535
|
+
throw new Error(reason || 'Workflow aborted');
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
try {
|
|
539
|
+
const handlerResult = await step.errorHandler(currentError, errorCtx);
|
|
540
|
+
// If retry succeeded, use that result and exit loop
|
|
541
|
+
if (retrySucceeded && retryResult) {
|
|
542
|
+
result = retryResult;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
// If no retry was requested, process the handler result and exit
|
|
546
|
+
if (!retryRequested) {
|
|
547
|
+
if (handlerResult &&
|
|
548
|
+
typeof handlerResult === 'object' &&
|
|
549
|
+
'__skip' in handlerResult) {
|
|
550
|
+
result = {
|
|
551
|
+
...result,
|
|
552
|
+
...handlerResult.result,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
else if (handlerResult !== undefined) {
|
|
556
|
+
result = { ...result, ...handlerResult };
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
// Retry was requested but succeeded, so we're done
|
|
561
|
+
if (retrySucceeded) {
|
|
562
|
+
result = retryResult;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (retryError) {
|
|
567
|
+
// Retry was attempted but failed
|
|
568
|
+
// Continue the loop to call the error handler again with the new error
|
|
569
|
+
currentError = retryError;
|
|
570
|
+
retrySucceeded = false;
|
|
571
|
+
retryResult = null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else if (workflowErrorHandler) {
|
|
576
|
+
// Try workflow-level error handler
|
|
577
|
+
const ctx = createContext(step.name);
|
|
578
|
+
const handlerResult = await workflowErrorHandler(error, ctx);
|
|
579
|
+
result = { ...result, ...handlerResult };
|
|
580
|
+
// Stop execution after workflow error handler (don't continue to next step)
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
// If there's a workflow timeout, wrap the entire execution
|
|
592
|
+
if (workflowTimeout) {
|
|
593
|
+
return await withTimeout(executeWithWorkflowTimeout(), workflowTimeout, 'workflow');
|
|
594
|
+
}
|
|
595
|
+
return await executeWithWorkflowTimeout();
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
if (workflowErrorHandler) {
|
|
599
|
+
const ctx = createContext();
|
|
600
|
+
return await workflowErrorHandler(error, ctx);
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Execute a single step
|
|
607
|
+
*/
|
|
608
|
+
async function executeStep(step, input, currentResult, createContext, defaultRetryConfig, _remainingTimeout) {
|
|
609
|
+
let result = { ...currentResult };
|
|
610
|
+
const retryConfig = step.retry || defaultRetryConfig;
|
|
611
|
+
switch (step.type) {
|
|
612
|
+
case 'step': {
|
|
613
|
+
const execute = async () => {
|
|
614
|
+
const ctx = createContext(step.name);
|
|
615
|
+
const stepInput = Object.keys(result).length > 0 ? result : input;
|
|
616
|
+
const stepResult = await step.fn(stepInput, ctx);
|
|
617
|
+
return { ...result, ...stepResult };
|
|
618
|
+
};
|
|
619
|
+
let executeWithTimeout = execute;
|
|
620
|
+
if (step.timeout) {
|
|
621
|
+
executeWithTimeout = () => withTimeout(execute(), step.timeout, step.name);
|
|
622
|
+
}
|
|
623
|
+
if (retryConfig) {
|
|
624
|
+
result = await withRetry(executeWithTimeout, retryConfig, step.name);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
result = await executeWithTimeout();
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'parallel': {
|
|
632
|
+
const execute = async () => {
|
|
633
|
+
const promises = step.parallelSteps.map(async (ps) => {
|
|
634
|
+
const ctx = createContext(ps.name);
|
|
635
|
+
const stepInput = Object.keys(result).length > 0 ? result : input;
|
|
636
|
+
const stepResult = await ps.fn(stepInput, ctx);
|
|
637
|
+
return { name: ps.name, result: stepResult };
|
|
638
|
+
});
|
|
639
|
+
const results = await Promise.all(promises);
|
|
640
|
+
const merged = { ...result };
|
|
641
|
+
for (const { name, result: r } of results) {
|
|
642
|
+
merged[name] = r;
|
|
643
|
+
}
|
|
644
|
+
return merged;
|
|
645
|
+
};
|
|
646
|
+
if (step.timeout) {
|
|
647
|
+
result = await withTimeout(execute(), step.timeout);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
result = await execute();
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case 'conditional': {
|
|
655
|
+
const ctx = createContext();
|
|
656
|
+
const conditionResult = await step.conditional.condition(ctx);
|
|
657
|
+
if (conditionResult) {
|
|
658
|
+
const thenResult = await step.conditional.thenBranch.build().execute(result);
|
|
659
|
+
result = { ...result, ...thenResult };
|
|
660
|
+
}
|
|
661
|
+
else if (step.conditional.elseBranch) {
|
|
662
|
+
const elseBranch = step.conditional.elseBranch;
|
|
663
|
+
if (elseBranch instanceof WorkflowBuilder) {
|
|
664
|
+
const elseResult = await elseBranch.build().execute(result);
|
|
665
|
+
result = { ...result, ...elseResult };
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const elseResult = await elseBranch(result, ctx);
|
|
669
|
+
result = { ...result, ...elseResult };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case 'loop': {
|
|
675
|
+
const { condition, body, options } = step.loop;
|
|
676
|
+
const maxIterations = options?.maxIterations ?? Infinity;
|
|
677
|
+
let iterations = 0;
|
|
678
|
+
while (true) {
|
|
679
|
+
const ctx = createContext();
|
|
680
|
+
ctx.result = result;
|
|
681
|
+
const shouldContinue = await condition(ctx);
|
|
682
|
+
if (!shouldContinue)
|
|
683
|
+
break;
|
|
684
|
+
iterations++;
|
|
685
|
+
if (iterations > maxIterations) {
|
|
686
|
+
if (options?.throwOnMaxIterations) {
|
|
687
|
+
throw new Error(`Max iterations exceeded: loop exceeded ${maxIterations} iterations`);
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
const loopResult = await body.build().execute(result);
|
|
692
|
+
result = { ...result, ...loopResult };
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
case 'forEach': {
|
|
697
|
+
const { itemsSelector, body, options } = step.forEach;
|
|
698
|
+
const ctx = createContext();
|
|
699
|
+
ctx.result = result;
|
|
700
|
+
const items = itemsSelector(ctx);
|
|
701
|
+
if (items.length === 0) {
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
const concurrency = options?.concurrency ?? 1;
|
|
705
|
+
const forEachResults = [];
|
|
706
|
+
if (concurrency === 1) {
|
|
707
|
+
// Sequential execution
|
|
708
|
+
for (let i = 0; i < items.length; i++) {
|
|
709
|
+
const item = items[i];
|
|
710
|
+
const itemInput = { item, index: i };
|
|
711
|
+
const itemResult = await body.build().execute(itemInput);
|
|
712
|
+
forEachResults.push(itemResult);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
// Parallel execution with concurrency limit
|
|
717
|
+
const chunks = [];
|
|
718
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
719
|
+
chunks.push(items.slice(i, i + concurrency));
|
|
720
|
+
}
|
|
721
|
+
let index = 0;
|
|
722
|
+
for (const chunk of chunks) {
|
|
723
|
+
const chunkPromises = chunk.map(async (item) => {
|
|
724
|
+
const currentIndex = index++;
|
|
725
|
+
const itemInput = { item, index: currentIndex };
|
|
726
|
+
return body.build().execute(itemInput);
|
|
727
|
+
});
|
|
728
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
729
|
+
forEachResults.push(...chunkResults);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
result = { ...result, forEachResults };
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
// ============================================================================
|
|
739
|
+
// Factory Function
|
|
740
|
+
// ============================================================================
|
|
741
|
+
/**
|
|
742
|
+
* Create a new workflow builder
|
|
743
|
+
*
|
|
744
|
+
* @param name - Workflow name (required)
|
|
745
|
+
* @returns WorkflowBuilder instance
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* ```typescript
|
|
749
|
+
* const orderWorkflow = workflow('order-process')
|
|
750
|
+
* .step('validate', async (input) => ({ valid: true }))
|
|
751
|
+
* .step('charge', async (input) => ({ charged: true }))
|
|
752
|
+
* .build()
|
|
753
|
+
*
|
|
754
|
+
* const result = await orderWorkflow.execute({ orderId: '123' })
|
|
755
|
+
* ```
|
|
756
|
+
*/
|
|
757
|
+
export function workflow(name) {
|
|
758
|
+
return new WorkflowBuilder(name);
|
|
759
|
+
}
|
|
760
|
+
// Re-export for convenience
|
|
761
|
+
export { workflow as default };
|
|
762
|
+
//# sourceMappingURL=workflow-builder.js.map
|