ai-workflows 2.1.1 → 2.3.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.
Files changed (211) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -1
  3. package/README.md +305 -184
  4. package/dist/barrier.d.ts +159 -0
  5. package/dist/barrier.d.ts.map +1 -0
  6. package/dist/barrier.js +377 -0
  7. package/dist/barrier.js.map +1 -0
  8. package/dist/cascade-context.d.ts +149 -0
  9. package/dist/cascade-context.d.ts.map +1 -0
  10. package/dist/cascade-context.js +324 -0
  11. package/dist/cascade-context.js.map +1 -0
  12. package/dist/cascade-executor.d.ts +196 -0
  13. package/dist/cascade-executor.d.ts.map +1 -0
  14. package/dist/cascade-executor.js +384 -0
  15. package/dist/cascade-executor.js.map +1 -0
  16. package/dist/context.d.ts.map +1 -1
  17. package/dist/context.js +27 -8
  18. package/dist/context.js.map +1 -1
  19. package/dist/cron-parser.d.ts +65 -0
  20. package/dist/cron-parser.d.ts.map +1 -0
  21. package/dist/cron-parser.js +294 -0
  22. package/dist/cron-parser.js.map +1 -0
  23. package/dist/cron-scheduler.d.ts +117 -0
  24. package/dist/cron-scheduler.d.ts.map +1 -0
  25. package/dist/cron-scheduler.js +176 -0
  26. package/dist/cron-scheduler.js.map +1 -0
  27. package/dist/database-context.d.ts +184 -0
  28. package/dist/database-context.d.ts.map +1 -0
  29. package/dist/database-context.js +428 -0
  30. package/dist/database-context.js.map +1 -0
  31. package/dist/dependency-graph.d.ts +157 -0
  32. package/dist/dependency-graph.d.ts.map +1 -0
  33. package/dist/dependency-graph.js +382 -0
  34. package/dist/dependency-graph.js.map +1 -0
  35. package/dist/digital-objects-adapter.d.ts +159 -0
  36. package/dist/digital-objects-adapter.d.ts.map +1 -0
  37. package/dist/digital-objects-adapter.js +229 -0
  38. package/dist/digital-objects-adapter.js.map +1 -0
  39. package/dist/durable-execution-cloudflare.d.ts +427 -0
  40. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  41. package/dist/durable-execution-cloudflare.js +510 -0
  42. package/dist/durable-execution-cloudflare.js.map +1 -0
  43. package/dist/durable-execution.d.ts +482 -0
  44. package/dist/durable-execution.d.ts.map +1 -0
  45. package/dist/durable-execution.js +594 -0
  46. package/dist/durable-execution.js.map +1 -0
  47. package/dist/durable-workflow.d.ts +176 -0
  48. package/dist/durable-workflow.d.ts.map +1 -0
  49. package/dist/durable-workflow.js +552 -0
  50. package/dist/durable-workflow.js.map +1 -0
  51. package/dist/every.d.ts +31 -2
  52. package/dist/every.d.ts.map +1 -1
  53. package/dist/every.js +63 -32
  54. package/dist/every.js.map +1 -1
  55. package/dist/graph/index.d.ts +8 -0
  56. package/dist/graph/index.d.ts.map +1 -0
  57. package/dist/graph/index.js +8 -0
  58. package/dist/graph/index.js.map +1 -0
  59. package/dist/graph/topological-sort.d.ts +121 -0
  60. package/dist/graph/topological-sort.d.ts.map +1 -0
  61. package/dist/graph/topological-sort.js +292 -0
  62. package/dist/graph/topological-sort.js.map +1 -0
  63. package/dist/index.d.ts +10 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +25 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/logger.d.ts +101 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +115 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/on.d.ts +35 -10
  72. package/dist/on.d.ts.map +1 -1
  73. package/dist/on.js +53 -19
  74. package/dist/on.js.map +1 -1
  75. package/dist/runtime.d.ts +169 -0
  76. package/dist/runtime.d.ts.map +1 -0
  77. package/dist/runtime.js +275 -0
  78. package/dist/runtime.js.map +1 -0
  79. package/dist/send.d.ts.map +1 -1
  80. package/dist/send.js +4 -3
  81. package/dist/send.js.map +1 -1
  82. package/dist/telemetry.d.ts +150 -0
  83. package/dist/telemetry.d.ts.map +1 -0
  84. package/dist/telemetry.js +388 -0
  85. package/dist/telemetry.js.map +1 -0
  86. package/dist/timer-registry.d.ts +77 -0
  87. package/dist/timer-registry.d.ts.map +1 -0
  88. package/dist/timer-registry.js +154 -0
  89. package/dist/timer-registry.js.map +1 -0
  90. package/dist/types.d.ts +105 -6
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/types.js +17 -1
  93. package/dist/types.js.map +1 -1
  94. package/dist/worker/durable-step.d.ts +481 -0
  95. package/dist/worker/durable-step.d.ts.map +1 -0
  96. package/dist/worker/durable-step.js +606 -0
  97. package/dist/worker/durable-step.js.map +1 -0
  98. package/dist/worker/index.d.ts +106 -0
  99. package/dist/worker/index.d.ts.map +1 -0
  100. package/dist/worker/index.js +124 -0
  101. package/dist/worker/index.js.map +1 -0
  102. package/dist/worker/state-adapter.d.ts +230 -0
  103. package/dist/worker/state-adapter.d.ts.map +1 -0
  104. package/dist/worker/state-adapter.js +409 -0
  105. package/dist/worker/state-adapter.js.map +1 -0
  106. package/dist/worker/topological-executor.d.ts +282 -0
  107. package/dist/worker/topological-executor.d.ts.map +1 -0
  108. package/dist/worker/topological-executor.js +396 -0
  109. package/dist/worker/topological-executor.js.map +1 -0
  110. package/dist/worker/workflow-builder.d.ts +286 -0
  111. package/dist/worker/workflow-builder.d.ts.map +1 -0
  112. package/dist/worker/workflow-builder.js +565 -0
  113. package/dist/worker/workflow-builder.js.map +1 -0
  114. package/dist/worker.d.ts +800 -0
  115. package/dist/worker.d.ts.map +1 -0
  116. package/dist/worker.js +2428 -0
  117. package/dist/worker.js.map +1 -0
  118. package/dist/workflow-builder.d.ts +287 -0
  119. package/dist/workflow-builder.d.ts.map +1 -0
  120. package/dist/workflow-builder.js +762 -0
  121. package/dist/workflow-builder.js.map +1 -0
  122. package/dist/workflow.d.ts +14 -30
  123. package/dist/workflow.d.ts.map +1 -1
  124. package/dist/workflow.js +136 -292
  125. package/dist/workflow.js.map +1 -1
  126. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  127. package/examples/02-content-moderation-cascade.ts +454 -0
  128. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  129. package/examples/04-database-persistence.ts +518 -0
  130. package/examples/README.md +173 -0
  131. package/package.json +21 -4
  132. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  133. package/src/__tests__/durable-workflow.test.ts +297 -0
  134. package/src/barrier.ts +507 -0
  135. package/src/cascade-context.ts +495 -0
  136. package/src/cascade-executor.ts +588 -0
  137. package/src/context.ts +51 -17
  138. package/src/cron-parser.ts +347 -0
  139. package/src/cron-scheduler.ts +239 -0
  140. package/src/database-context.ts +658 -0
  141. package/src/dependency-graph.ts +518 -0
  142. package/src/digital-objects-adapter.ts +351 -0
  143. package/src/durable-execution-cloudflare.ts +855 -0
  144. package/src/durable-execution.ts +1042 -0
  145. package/src/durable-workflow.ts +717 -0
  146. package/src/every.ts +104 -35
  147. package/src/graph/index.ts +19 -0
  148. package/src/graph/topological-sort.ts +412 -0
  149. package/src/index.ts +147 -0
  150. package/src/logger.ts +148 -0
  151. package/src/on.ts +81 -26
  152. package/src/runtime.ts +436 -0
  153. package/src/send.ts +4 -5
  154. package/src/telemetry.ts +577 -0
  155. package/src/timer-registry.ts +179 -0
  156. package/src/types.ts +146 -10
  157. package/src/worker/durable-step.ts +976 -0
  158. package/src/worker/index.ts +216 -0
  159. package/src/worker/state-adapter.ts +589 -0
  160. package/src/worker/topological-executor.ts +625 -0
  161. package/src/worker/workflow-builder.ts +871 -0
  162. package/src/worker.ts +2906 -0
  163. package/src/workflow-builder.ts +1068 -0
  164. package/src/workflow.ts +199 -355
  165. package/test/barrier-join.test.ts +442 -0
  166. package/test/barrier-unhandled-rejections.test.ts +359 -0
  167. package/test/cascade-context.test.ts +390 -0
  168. package/test/cascade-executor.test.ts +852 -0
  169. package/test/cron-parser.test.ts +314 -0
  170. package/test/cron-scheduler.test.ts +291 -0
  171. package/test/database-context.test.ts +770 -0
  172. package/test/db-provider-adapter.test.ts +862 -0
  173. package/test/dependency-graph.test.ts +512 -0
  174. package/test/durable-execution-cloudflare.test.ts +606 -0
  175. package/test/durable-execution-in-process.test.ts +286 -0
  176. package/test/durable-execution.test.ts +247 -0
  177. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  178. package/test/graph/topological-sort.test.ts +586 -0
  179. package/test/integration.test.ts +442 -0
  180. package/test/rpc-surface.test.ts +946 -0
  181. package/test/runtime.test.ts +262 -0
  182. package/test/schedule-timer-cleanup.test.ts +353 -0
  183. package/test/send-race-conditions.test.ts +400 -0
  184. package/test/type-safety-every.test.ts +303 -0
  185. package/test/worker/durable-cascade.test.ts +1117 -0
  186. package/test/worker/durable-step.test.ts +723 -0
  187. package/test/worker/topological-executor.test.ts +1240 -0
  188. package/test/worker/workflow-builder.test.ts +1067 -0
  189. package/test/worker.test.ts +608 -0
  190. package/test/workflow-builder.test.ts +1670 -0
  191. package/test/workflow-cron.test.ts +256 -0
  192. package/test/workflow-state-adapter.test.ts +923 -0
  193. package/test/workflow.test.ts +25 -22
  194. package/tsconfig.json +3 -1
  195. package/vitest.config.ts +38 -1
  196. package/vitest.workers.config.ts +44 -0
  197. package/wrangler.jsonc +22 -0
  198. package/.turbo/turbo-test.log +0 -7
  199. package/src/context.js +0 -83
  200. package/src/every.js +0 -267
  201. package/src/index.js +0 -71
  202. package/src/on.js +0 -79
  203. package/src/send.js +0 -111
  204. package/src/types.js +0 -4
  205. package/src/workflow.js +0 -455
  206. package/test/context.test.js +0 -116
  207. package/test/every.test.js +0 -282
  208. package/test/on.test.js +0 -80
  209. package/test/send.test.js +0 -89
  210. package/test/workflow.test.js +0 -224
  211. 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