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.
Files changed (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +14 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
@@ -0,0 +1,625 @@
1
+ /**
2
+ * TopologicalExecutor - Parallel step execution based on dependency graph
3
+ *
4
+ * Executes workflow steps in parallel while respecting dependency ordering.
5
+ * Uses topological sort to determine execution levels and runs steps
6
+ * within each level concurrently.
7
+ *
8
+ * Features:
9
+ * - Parallel execution of independent steps
10
+ * - Durable execution via Cloudflare Workflows step.do()
11
+ * - Progress callbacks for monitoring
12
+ * - Error handling with rollback support
13
+ * - Visualization helpers for debugging
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { TopologicalExecutor, DurableGraph } from 'ai-workflows/worker'
18
+ *
19
+ * // Create executor with steps
20
+ * const executor = new TopologicalExecutor()
21
+ * .addStep('fetch-user', [], async (input) => {
22
+ * return { userId: input.id }
23
+ * })
24
+ * .addStep('fetch-orders', ['fetch-user'], async (input, results) => {
25
+ * return { orders: [] }
26
+ * })
27
+ * .addStep('fetch-prefs', ['fetch-user'], async (input, results) => {
28
+ * return { prefs: {} }
29
+ * })
30
+ * .addStep('aggregate', ['fetch-orders', 'fetch-prefs'], async (input, results) => {
31
+ * return { ...results['fetch-orders'], ...results['fetch-prefs'] }
32
+ * })
33
+ *
34
+ * // Execute with durable workflow
35
+ * const result = await executor.run(step, { id: 'user-123' })
36
+ * ```
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+
41
+ import {
42
+ DependencyGraph,
43
+ CircularDependencyError,
44
+ MissingDependencyError,
45
+ type GraphNode,
46
+ type ParallelGroup,
47
+ type DependencyType,
48
+ } from '../dependency-graph.js'
49
+
50
+ import {
51
+ topologicalSort,
52
+ getExecutionLevels,
53
+ CycleDetectedError,
54
+ type SortableNode,
55
+ type ExecutionLevel,
56
+ } from '../graph/topological-sort.js'
57
+
58
+ import type { WorkflowStep, StepConfig } from './durable-step.js'
59
+
60
+ // ============================================================================
61
+ // Types
62
+ // ============================================================================
63
+
64
+ /**
65
+ * Function signature for step execution
66
+ */
67
+ export type StepExecutor<TInput = unknown, TOutput = unknown> = (
68
+ input: TInput,
69
+ previousResults: Record<string, unknown>,
70
+ context: ExecutionContext
71
+ ) => Promise<TOutput>
72
+
73
+ /**
74
+ * Definition of a step in the execution graph
75
+ */
76
+ export interface StepDefinition<TInput = unknown, TOutput = unknown> {
77
+ /** Unique step identifier */
78
+ id: string
79
+ /** IDs of steps this step depends on */
80
+ dependencies: string[]
81
+ /** The step function to execute */
82
+ executor: StepExecutor<TInput, TOutput>
83
+ /** Optional step configuration (retries, timeout) */
84
+ config: StepConfig | undefined
85
+ /** Dependency types (hard/soft) */
86
+ dependencyTypes: Record<string, DependencyType> | undefined
87
+ }
88
+
89
+ /**
90
+ * Context provided during step execution
91
+ */
92
+ export interface ExecutionContext {
93
+ /** Current step ID */
94
+ stepId: string
95
+ /** Current execution level */
96
+ level: number
97
+ /** Total number of levels */
98
+ totalLevels: number
99
+ /** Steps completed so far */
100
+ completedSteps: string[]
101
+ /** Attempt number for this step */
102
+ attempt: number
103
+ }
104
+
105
+ /**
106
+ * Progress callback for monitoring execution
107
+ */
108
+ export interface ProgressCallback {
109
+ /** Called when a level starts */
110
+ onLevelStart?: (level: number, steps: string[]) => void
111
+ /** Called when a step starts */
112
+ onStepStart?: (stepId: string, level: number) => void
113
+ /** Called when a step completes */
114
+ onStepComplete?: (stepId: string, level: number, result: unknown, duration: number) => void
115
+ /** Called when a step fails */
116
+ onStepError?: (stepId: string, level: number, error: Error) => void
117
+ /** Called when a level completes */
118
+ onLevelComplete?: (level: number, results: Record<string, unknown>) => void
119
+ }
120
+
121
+ /**
122
+ * Result of executing the graph
123
+ */
124
+ export interface ExecutionResult<T = Record<string, unknown>> {
125
+ /** All step results keyed by step ID */
126
+ results: T
127
+ /** Execution metrics */
128
+ metrics: {
129
+ totalDuration: number
130
+ levelDurations: number[]
131
+ stepDurations: Record<string, number>
132
+ parallelEfficiency: number
133
+ }
134
+ /** Steps that were skipped due to soft dependency failures */
135
+ skippedSteps: string[]
136
+ /** Execution order (actual order steps ran in) */
137
+ executionOrder: string[]
138
+ }
139
+
140
+ /**
141
+ * Configuration for TopologicalExecutor
142
+ */
143
+ export interface ExecutorConfig {
144
+ /** Continue execution on step failure (for soft dependencies) */
145
+ continueOnError?: boolean
146
+ /** Maximum concurrency per level (default: unlimited) */
147
+ maxConcurrency?: number
148
+ /** Progress callbacks */
149
+ progress?: ProgressCallback
150
+ /** Default step config */
151
+ defaultStepConfig?: StepConfig
152
+ }
153
+
154
+ // ============================================================================
155
+ // TopologicalExecutor
156
+ // ============================================================================
157
+
158
+ /**
159
+ * TopologicalExecutor - Execute steps in parallel based on dependency graph
160
+ *
161
+ * Provides parallel execution of workflow steps while respecting dependencies.
162
+ * Steps at the same execution level run concurrently, while levels execute
163
+ * sequentially.
164
+ */
165
+ export class TopologicalExecutor<TInput = unknown> {
166
+ private steps: Map<string, StepDefinition> = new Map()
167
+ private config: ExecutorConfig
168
+
169
+ constructor(config: ExecutorConfig = {}) {
170
+ this.config = config
171
+ }
172
+
173
+ /**
174
+ * Add a step to the executor
175
+ *
176
+ * @param id - Unique step identifier
177
+ * @param dependencies - IDs of steps this step depends on
178
+ * @param executor - Function to execute
179
+ * @param config - Optional step configuration
180
+ * @returns this (for chaining)
181
+ */
182
+ addStep<TOutput>(
183
+ id: string,
184
+ dependencies: string[],
185
+ executor: StepExecutor<TInput, TOutput>,
186
+ config?: StepConfig
187
+ ): this {
188
+ this.steps.set(id, {
189
+ id,
190
+ dependencies,
191
+ executor: executor as StepExecutor,
192
+ config: config ?? this.config.defaultStepConfig,
193
+ dependencyTypes: undefined,
194
+ })
195
+ return this
196
+ }
197
+
198
+ /**
199
+ * Add a step with dependency type specification
200
+ */
201
+ addStepWithTypes<TOutput>(
202
+ id: string,
203
+ dependencies: Array<{ id: string; type?: DependencyType }>,
204
+ executor: StepExecutor<TInput, TOutput>,
205
+ config?: StepConfig
206
+ ): this {
207
+ const depIds = dependencies.map((d) => d.id)
208
+ const depTypes: Record<string, DependencyType> = {}
209
+ for (const dep of dependencies) {
210
+ depTypes[dep.id] = dep.type ?? 'hard'
211
+ }
212
+
213
+ this.steps.set(id, {
214
+ id,
215
+ dependencies: depIds,
216
+ executor: executor as StepExecutor,
217
+ config: config ?? this.config.defaultStepConfig,
218
+ dependencyTypes: depTypes,
219
+ })
220
+ return this
221
+ }
222
+
223
+ /**
224
+ * Get the dependency graph
225
+ */
226
+ getGraph(): DependencyGraph {
227
+ const graph = new DependencyGraph()
228
+
229
+ // Add nodes in topological order to ensure dependencies exist
230
+ const nodes: SortableNode[] = Array.from(this.steps.values()).map((s) => ({
231
+ id: s.id,
232
+ dependencies: s.dependencies,
233
+ }))
234
+
235
+ const sorted = topologicalSort(nodes)
236
+ if (sorted.hasCycle) {
237
+ throw new CycleDetectedError(sorted.cyclePath ?? ['unknown'])
238
+ }
239
+
240
+ // Add nodes without dependencies first
241
+ for (const id of sorted.order) {
242
+ const step = this.steps.get(id)!
243
+ if (step.dependencies.length === 0) {
244
+ graph.addNode(id)
245
+ }
246
+ }
247
+
248
+ // Add nodes with dependencies
249
+ for (const id of sorted.order) {
250
+ const step = this.steps.get(id)!
251
+ if (step.dependencies.length > 0) {
252
+ const firstDep = step.dependencies[0]
253
+ const depType = firstDep !== undefined ? step.dependencyTypes?.[firstDep] ?? 'hard' : 'hard'
254
+ graph.addNode(id, {
255
+ dependsOn: step.dependencies,
256
+ type: depType,
257
+ })
258
+ }
259
+ }
260
+
261
+ return graph
262
+ }
263
+
264
+ /**
265
+ * Get execution levels for parallel execution
266
+ */
267
+ getExecutionLevels(): ExecutionLevel[] {
268
+ const nodes: SortableNode[] = Array.from(this.steps.values()).map((s) => ({
269
+ id: s.id,
270
+ dependencies: s.dependencies,
271
+ }))
272
+ return getExecutionLevels(nodes)
273
+ }
274
+
275
+ /**
276
+ * Validate the graph (check for cycles, missing deps)
277
+ */
278
+ validate(): { valid: boolean; errors: string[] } {
279
+ const errors: string[] = []
280
+
281
+ const nodes: SortableNode[] = Array.from(this.steps.values()).map((s) => ({
282
+ id: s.id,
283
+ dependencies: s.dependencies,
284
+ }))
285
+
286
+ // Check for cycles
287
+ const result = topologicalSort(nodes)
288
+ if (result.hasCycle) {
289
+ errors.push(`Circular dependency detected: ${result.cyclePath?.join(' -> ')}`)
290
+ }
291
+
292
+ // Check for missing dependencies
293
+ const stepIds = new Set(this.steps.keys())
294
+ for (const step of this.steps.values()) {
295
+ for (const dep of step.dependencies) {
296
+ if (!stepIds.has(dep)) {
297
+ errors.push(`Step '${step.id}' depends on missing step '${dep}'`)
298
+ }
299
+ }
300
+ }
301
+
302
+ return {
303
+ valid: errors.length === 0,
304
+ errors,
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Execute the graph with durable semantics
310
+ *
311
+ * @param workflowStep - Cloudflare Workflows step object
312
+ * @param input - Input to pass to all steps
313
+ * @returns Execution result with all step outputs
314
+ */
315
+ async run(
316
+ workflowStep: WorkflowStep,
317
+ input: TInput
318
+ ): Promise<ExecutionResult<Record<string, unknown>>> {
319
+ const validation = this.validate()
320
+ if (!validation.valid) {
321
+ throw new Error(`Invalid execution graph: ${validation.errors.join(', ')}`)
322
+ }
323
+
324
+ const levels = this.getExecutionLevels()
325
+ const results: Record<string, unknown> = {}
326
+ const stepDurations: Record<string, number> = {}
327
+ const levelDurations: number[] = []
328
+ const skippedSteps: string[] = []
329
+ const executionOrder: string[] = []
330
+ const completedSteps: string[] = []
331
+ const failedSteps = new Set<string>()
332
+
333
+ const startTime = Date.now()
334
+
335
+ for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
336
+ const level = levels[levelIndex]!
337
+ const levelStartTime = Date.now()
338
+
339
+ this.config.progress?.onLevelStart?.(level.level, level.nodes)
340
+
341
+ // Filter steps that can run (all hard deps satisfied)
342
+ const runnableSteps = level.nodes.filter((stepId) => {
343
+ const step = this.steps.get(stepId)!
344
+ const hardDeps = step.dependencies.filter(
345
+ (dep) => (step.dependencyTypes?.[dep] ?? 'hard') === 'hard'
346
+ )
347
+ return hardDeps.every((dep) => completedSteps.includes(dep))
348
+ })
349
+
350
+ // Skip steps whose hard dependencies failed
351
+ const stepsToSkip = level.nodes.filter((stepId) => !runnableSteps.includes(stepId))
352
+ for (const stepId of stepsToSkip) {
353
+ skippedSteps.push(stepId)
354
+ failedSteps.add(stepId)
355
+ }
356
+
357
+ // Execute runnable steps in parallel
358
+ const stepPromises = runnableSteps.map(async (stepId) => {
359
+ const step = this.steps.get(stepId)!
360
+ const stepStartTime = Date.now()
361
+
362
+ this.config.progress?.onStepStart?.(stepId, level.level)
363
+
364
+ const context: ExecutionContext = {
365
+ stepId,
366
+ level: level.level,
367
+ totalLevels: levels.length,
368
+ completedSteps: [...completedSteps],
369
+ attempt: 1,
370
+ }
371
+
372
+ try {
373
+ const result = await this.executeStep(workflowStep, step, input, results, context)
374
+ const duration = Date.now() - stepStartTime
375
+
376
+ results[stepId] = result
377
+ stepDurations[stepId] = duration
378
+ executionOrder.push(stepId)
379
+ completedSteps.push(stepId)
380
+
381
+ this.config.progress?.onStepComplete?.(stepId, level.level, result, duration)
382
+
383
+ return { stepId, success: true, result }
384
+ } catch (error) {
385
+ const err = error instanceof Error ? error : new Error(String(error))
386
+ const duration = Date.now() - stepStartTime
387
+
388
+ stepDurations[stepId] = duration
389
+ failedSteps.add(stepId)
390
+
391
+ this.config.progress?.onStepError?.(stepId, level.level, err)
392
+
393
+ if (!this.config.continueOnError) {
394
+ throw err
395
+ }
396
+
397
+ return { stepId, success: false, error: err }
398
+ }
399
+ })
400
+
401
+ // Wait for all steps in this level
402
+ const levelResults = await Promise.all(stepPromises)
403
+ const levelDuration = Date.now() - levelStartTime
404
+ levelDurations.push(levelDuration)
405
+
406
+ this.config.progress?.onLevelComplete?.(level.level, results)
407
+ }
408
+
409
+ const totalDuration = Date.now() - startTime
410
+
411
+ // Calculate parallel efficiency
412
+ const sequentialTime = Object.values(stepDurations).reduce((a, b) => a + b, 0)
413
+ const parallelEfficiency = sequentialTime > 0 ? sequentialTime / totalDuration : 1
414
+
415
+ return {
416
+ results,
417
+ metrics: {
418
+ totalDuration,
419
+ levelDurations,
420
+ stepDurations,
421
+ parallelEfficiency,
422
+ },
423
+ skippedSteps,
424
+ executionOrder,
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Execute a single step with durability
430
+ */
431
+ private async executeStep(
432
+ workflowStep: WorkflowStep,
433
+ step: StepDefinition,
434
+ input: TInput,
435
+ previousResults: Record<string, unknown>,
436
+ context: ExecutionContext
437
+ ): Promise<unknown> {
438
+ if (step.config) {
439
+ return workflowStep.do(step.id, step.config, async () => {
440
+ return step.executor(input, previousResults, context)
441
+ })
442
+ } else {
443
+ return workflowStep.do(step.id, async () => {
444
+ return step.executor(input, previousResults, context)
445
+ })
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Visualize the execution graph as DOT format
451
+ */
452
+ toDot(): string {
453
+ const graph = this.getGraph()
454
+ return graph.toDot()
455
+ }
456
+
457
+ /**
458
+ * Get graph as JSON for debugging
459
+ */
460
+ toJSON(): { steps: StepDefinition[]; levels: ExecutionLevel[] } {
461
+ return {
462
+ steps: Array.from(this.steps.values()),
463
+ levels: this.getExecutionLevels(),
464
+ }
465
+ }
466
+ }
467
+
468
+ // ============================================================================
469
+ // DurableGraph
470
+ // ============================================================================
471
+
472
+ /**
473
+ * DurableGraph - Dependency graph with durable execution support
474
+ *
475
+ * Extends DependencyGraph with execution capabilities using Cloudflare
476
+ * Workflows durability. Provides a higher-level API for building and
477
+ * executing workflow step graphs.
478
+ *
479
+ * @example
480
+ * ```typescript
481
+ * const graph = new DurableGraph<{ userId: string }>()
482
+ * .node('fetch-user', async (input) => {
483
+ * return await fetchUser(input.userId)
484
+ * })
485
+ * .node('fetch-orders', ['fetch-user'], async (input, results) => {
486
+ * return await fetchOrders(results['fetch-user'].id)
487
+ * })
488
+ *
489
+ * const result = await graph.execute(step, { userId: '123' })
490
+ * ```
491
+ */
492
+ export class DurableGraph<TInput = unknown> {
493
+ private executor: TopologicalExecutor<TInput>
494
+ private nodeMetadata: Map<string, { description?: string; tags?: string[] }> = new Map()
495
+
496
+ constructor(config: ExecutorConfig = {}) {
497
+ this.executor = new TopologicalExecutor<TInput>(config)
498
+ }
499
+
500
+ /**
501
+ * Add a node (step) to the graph
502
+ *
503
+ * @param id - Unique node identifier
504
+ * @param executor - Function to execute (if no dependencies)
505
+ */
506
+ node<TOutput>(id: string, executor: StepExecutor<TInput, TOutput>): this
507
+
508
+ /**
509
+ * Add a node with dependencies
510
+ *
511
+ * @param id - Unique node identifier
512
+ * @param dependencies - IDs of nodes this node depends on
513
+ * @param executor - Function to execute
514
+ */
515
+ node<TOutput>(id: string, dependencies: string[], executor: StepExecutor<TInput, TOutput>): this
516
+
517
+ /**
518
+ * Add a node with dependencies and config
519
+ *
520
+ * @param id - Unique node identifier
521
+ * @param dependencies - IDs of nodes this node depends on
522
+ * @param executor - Function to execute
523
+ * @param config - Step configuration
524
+ */
525
+ node<TOutput>(
526
+ id: string,
527
+ dependencies: string[],
528
+ executor: StepExecutor<TInput, TOutput>,
529
+ config: StepConfig
530
+ ): this
531
+
532
+ /**
533
+ * Implementation of node() overloads
534
+ */
535
+ node<TOutput>(
536
+ id: string,
537
+ depsOrExecutor: string[] | StepExecutor<TInput, TOutput>,
538
+ executorOrConfig?: StepExecutor<TInput, TOutput> | StepConfig,
539
+ maybeConfig?: StepConfig
540
+ ): this {
541
+ if (typeof depsOrExecutor === 'function') {
542
+ // node(id, executor)
543
+ this.executor.addStep(id, [], depsOrExecutor)
544
+ } else if (typeof executorOrConfig === 'function') {
545
+ // node(id, deps, executor) or node(id, deps, executor, config)
546
+ this.executor.addStep(id, depsOrExecutor, executorOrConfig, maybeConfig)
547
+ }
548
+ return this
549
+ }
550
+
551
+ /**
552
+ * Add metadata to a node
553
+ */
554
+ describe(id: string, metadata: { description?: string; tags?: string[] }): this {
555
+ this.nodeMetadata.set(id, metadata)
556
+ return this
557
+ }
558
+
559
+ /**
560
+ * Get execution levels
561
+ */
562
+ levels(): ExecutionLevel[] {
563
+ return this.executor.getExecutionLevels()
564
+ }
565
+
566
+ /**
567
+ * Validate the graph
568
+ */
569
+ validate(): { valid: boolean; errors: string[] } {
570
+ return this.executor.validate()
571
+ }
572
+
573
+ /**
574
+ * Execute the graph with durable semantics
575
+ */
576
+ async execute(
577
+ workflowStep: WorkflowStep,
578
+ input: TInput
579
+ ): Promise<ExecutionResult<Record<string, unknown>>> {
580
+ return this.executor.run(workflowStep, input)
581
+ }
582
+
583
+ /**
584
+ * Get DOT visualization
585
+ */
586
+ toDot(): string {
587
+ return this.executor.toDot()
588
+ }
589
+
590
+ /**
591
+ * Get as JSON
592
+ */
593
+ toJSON(): ReturnType<TopologicalExecutor<TInput>['toJSON']> & {
594
+ metadata: Record<string, { description?: string; tags?: string[] }>
595
+ } {
596
+ const metadata: Record<string, { description?: string; tags?: string[] }> = {}
597
+ for (const [id, meta] of this.nodeMetadata) {
598
+ metadata[id] = meta
599
+ }
600
+ return {
601
+ ...this.executor.toJSON(),
602
+ metadata,
603
+ }
604
+ }
605
+ }
606
+
607
+ // ============================================================================
608
+ // Convenience functions
609
+ // ============================================================================
610
+
611
+ /**
612
+ * Create a new TopologicalExecutor
613
+ */
614
+ export function createExecutor<TInput = unknown>(
615
+ config?: ExecutorConfig
616
+ ): TopologicalExecutor<TInput> {
617
+ return new TopologicalExecutor<TInput>(config)
618
+ }
619
+
620
+ /**
621
+ * Create a new DurableGraph
622
+ */
623
+ export function createGraph<TInput = unknown>(config?: ExecutorConfig): DurableGraph<TInput> {
624
+ return new DurableGraph<TInput>(config)
625
+ }