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,495 @@
1
+ /**
2
+ * Cascade Context - Correlation IDs and step metadata for workflow execution
3
+ *
4
+ * Provides distributed tracing support with:
5
+ * - Unique correlation IDs per cascade
6
+ * - Step timing metadata (started_at, completed_at, duration)
7
+ * - Cascade path recording
8
+ * - Context inheritance for nested operations
9
+ * - Serialization for distributed systems
10
+ * - W3C Trace Context compatibility
11
+ */
12
+
13
+ /**
14
+ * Step status in the cascade
15
+ */
16
+ export type StepStatus = 'running' | 'completed' | 'failed'
17
+
18
+ /**
19
+ * 5W+H Event structure
20
+ */
21
+ export interface FiveWHEvent {
22
+ who: string
23
+ what: string
24
+ when: number
25
+ where: string
26
+ why?: string
27
+ how: {
28
+ duration?: number
29
+ status: StepStatus
30
+ metadata?: Record<string, unknown>
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Cascade step with timing and metadata
36
+ */
37
+ export interface CascadeStep {
38
+ /** Step name */
39
+ name: string
40
+ /** When the step started (unix timestamp ms) */
41
+ startedAt: number
42
+ /** When the step completed (unix timestamp ms) */
43
+ completedAt?: number
44
+ /** Duration in milliseconds */
45
+ duration?: number
46
+ /** Current status */
47
+ status: StepStatus
48
+ /** Error if failed */
49
+ error?: Error
50
+ /** Additional metadata */
51
+ metadata?: Record<string, unknown>
52
+ /** Mark step as completed */
53
+ complete: () => void
54
+ /** Mark step as failed */
55
+ fail: (error: Error) => void
56
+ /** Add metadata to the step */
57
+ addMetadata: (data: Record<string, unknown>) => void
58
+ /** Convert step to 5W+H event format */
59
+ to5WHEvent: () => FiveWHEvent
60
+ }
61
+
62
+ /**
63
+ * Serialized cascade step (for transmission/storage)
64
+ */
65
+ export interface SerializedCascadeStep {
66
+ name: string
67
+ startedAt: number
68
+ completedAt?: number
69
+ duration?: number
70
+ status: StepStatus
71
+ metadata?: Record<string, unknown>
72
+ }
73
+
74
+ /**
75
+ * W3C Trace Context
76
+ */
77
+ export interface TraceContext {
78
+ traceparent: string
79
+ tracestate?: string
80
+ }
81
+
82
+ /**
83
+ * Serialized cascade context
84
+ */
85
+ export interface SerializedCascadeContext {
86
+ correlationId: string
87
+ spanId: string
88
+ parentId?: string
89
+ name?: string
90
+ depth: number
91
+ steps: SerializedCascadeStep[]
92
+ path: string[]
93
+ createdAt: number
94
+ }
95
+
96
+ /**
97
+ * Cascade context for tracking workflow execution
98
+ */
99
+ export interface CascadeContext {
100
+ /** Unique correlation ID for this cascade (propagates to children) */
101
+ correlationId: string
102
+ /** Unique span ID for this context */
103
+ spanId: string
104
+ /** Parent span ID (if this is a child context) */
105
+ parentId?: string
106
+ /** Name of this cascade/context */
107
+ name?: string
108
+ /** Depth in the context hierarchy (0 = root) */
109
+ depth: number
110
+ /** Steps recorded in this context */
111
+ steps: CascadeStep[]
112
+ /** Path of step names in this context */
113
+ path: string[]
114
+ /** Full path including parent context steps */
115
+ fullPath: string[]
116
+ /** When the context was created */
117
+ createdAt: number
118
+ /** Parent context reference */
119
+ parent?: CascadeContext
120
+ /** Serialize context for transmission */
121
+ serialize: () => SerializedCascadeContext
122
+ /** Format context as readable string */
123
+ format: () => string
124
+ /** Format as tree including parent contexts */
125
+ formatTree: () => string
126
+ /** Convert to W3C Trace Context format */
127
+ toTraceContext: () => TraceContext
128
+ }
129
+
130
+ /**
131
+ * Options for creating a cascade context
132
+ */
133
+ export interface CascadeContextOptions {
134
+ /** Custom correlation ID (auto-generated if not provided) */
135
+ correlationId?: string
136
+ /** Parent context (for nested operations) */
137
+ parent?: CascadeContext
138
+ /** Name for this cascade */
139
+ name?: string
140
+ /** Restore from serialized format */
141
+ fromSerialized?: SerializedCascadeContext
142
+ /** Restore from W3C trace context */
143
+ fromTraceContext?: TraceContext
144
+ }
145
+
146
+ /**
147
+ * Generate a UUID v4
148
+ */
149
+ function generateUUID(): string {
150
+ // Use crypto.randomUUID if available, otherwise fallback
151
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
152
+ return crypto.randomUUID()
153
+ }
154
+ // Fallback UUID v4 generation
155
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
156
+ const r = (Math.random() * 16) | 0
157
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
158
+ return v.toString(16)
159
+ })
160
+ }
161
+
162
+ /**
163
+ * Generate a span ID (16 hex characters)
164
+ */
165
+ function generateSpanId(): string {
166
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
167
+ return crypto.randomUUID().replace(/-/g, '').slice(0, 16)
168
+ }
169
+ return 'xxxxxxxxxxxxxxxx'.replace(/x/g, () => {
170
+ return ((Math.random() * 16) | 0).toString(16)
171
+ })
172
+ }
173
+
174
+ /**
175
+ * Parse W3C traceparent header
176
+ */
177
+ function parseTraceparent(traceparent: string): { traceId: string; parentId: string } | null {
178
+ const parts = traceparent.split('-')
179
+ if (parts.length !== 4) return null
180
+ return {
181
+ traceId: parts[1]!, // 32 hex chars
182
+ parentId: parts[2]!, // 16 hex chars
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Convert correlation ID to trace ID format (32 hex chars)
188
+ */
189
+ function correlationIdToTraceId(correlationId: string): string {
190
+ // Remove dashes and pad/truncate to 32 chars
191
+ const hex = correlationId.replace(/-/g, '')
192
+ return hex.slice(0, 32).padEnd(32, '0')
193
+ }
194
+
195
+ /**
196
+ * Convert trace ID to correlation ID format (UUID)
197
+ */
198
+ function traceIdToCorrelationId(traceId: string): string {
199
+ // Convert 32 hex chars to UUID format
200
+ return `${traceId.slice(0, 8)}-${traceId.slice(8, 12)}-${traceId.slice(12, 16)}-${traceId.slice(
201
+ 16,
202
+ 20
203
+ )}-${traceId.slice(20, 32)}`
204
+ }
205
+
206
+ /**
207
+ * Create a cascade context for tracking workflow execution
208
+ */
209
+ export function createCascadeContext(options: CascadeContextOptions = {}): CascadeContext {
210
+ const { parent, name, fromSerialized, fromTraceContext } = options
211
+
212
+ // Handle restoration from serialized format
213
+ if (fromSerialized) {
214
+ const restoredSteps: CascadeStep[] = fromSerialized.steps.map((s) =>
215
+ createRestoredStep(s, name)
216
+ )
217
+
218
+ const ctx: CascadeContext = {
219
+ correlationId: fromSerialized.correlationId,
220
+ spanId: fromSerialized.spanId,
221
+ ...(fromSerialized.parentId !== undefined && { parentId: fromSerialized.parentId }),
222
+ ...(fromSerialized.name !== undefined && { name: fromSerialized.name }),
223
+ depth: fromSerialized.depth,
224
+ steps: restoredSteps,
225
+ path: fromSerialized.path,
226
+ createdAt: fromSerialized.createdAt,
227
+ get fullPath() {
228
+ return this.path
229
+ },
230
+ serialize: () => serializeContext(ctx),
231
+ format: () => formatContext(ctx),
232
+ formatTree: () => formatContextTree(ctx),
233
+ toTraceContext: () => toTraceContext(ctx),
234
+ }
235
+ return ctx
236
+ }
237
+
238
+ // Handle restoration from W3C trace context
239
+ if (fromTraceContext) {
240
+ const parsed = parseTraceparent(fromTraceContext.traceparent)
241
+ if (parsed) {
242
+ const correlationId = traceIdToCorrelationId(parsed.traceId)
243
+ const newSpanId = generateSpanId()
244
+
245
+ const ctx: CascadeContext = {
246
+ correlationId,
247
+ spanId: newSpanId,
248
+ parentId: parsed.parentId,
249
+ ...(name !== undefined && { name }),
250
+ depth: 0,
251
+ steps: [],
252
+ path: [],
253
+ createdAt: Date.now(),
254
+ get fullPath() {
255
+ return this.path
256
+ },
257
+ serialize: () => serializeContext(ctx),
258
+ format: () => formatContext(ctx),
259
+ formatTree: () => formatContextTree(ctx),
260
+ toTraceContext: () => toTraceContext(ctx),
261
+ }
262
+ return ctx
263
+ }
264
+ }
265
+
266
+ // Determine correlation ID
267
+ let correlationId: string
268
+ if (parent) {
269
+ correlationId = parent.correlationId
270
+ } else if (options.correlationId) {
271
+ correlationId = options.correlationId
272
+ } else {
273
+ correlationId = generateUUID()
274
+ }
275
+
276
+ const spanId = generateSpanId()
277
+ const parentId = parent?.spanId
278
+ const depth = parent ? parent.depth + 1 : 0
279
+ const createdAt = Date.now()
280
+
281
+ const steps: CascadeStep[] = []
282
+ const path: string[] = []
283
+
284
+ const ctx: CascadeContext = {
285
+ correlationId,
286
+ spanId,
287
+ ...(parentId !== undefined && { parentId }),
288
+ ...(name !== undefined && { name }),
289
+ depth,
290
+ steps,
291
+ path,
292
+ createdAt,
293
+ ...(parent !== undefined && { parent }),
294
+ get fullPath() {
295
+ if (this.parent) {
296
+ return [...this.parent.fullPath, ...this.path]
297
+ }
298
+ return this.path
299
+ },
300
+ serialize: () => serializeContext(ctx),
301
+ format: () => formatContext(ctx),
302
+ formatTree: () => formatContextTree(ctx),
303
+ toTraceContext: () => toTraceContext(ctx),
304
+ }
305
+
306
+ return ctx
307
+ }
308
+
309
+ /**
310
+ * Create a restored step from serialized data
311
+ */
312
+ function createRestoredStep(serialized: SerializedCascadeStep, contextName?: string): CascadeStep {
313
+ const step: CascadeStep = {
314
+ name: serialized.name,
315
+ startedAt: serialized.startedAt,
316
+ ...(serialized.completedAt !== undefined && { completedAt: serialized.completedAt }),
317
+ ...(serialized.duration !== undefined && { duration: serialized.duration }),
318
+ status: serialized.status,
319
+ ...(serialized.metadata !== undefined && { metadata: { ...serialized.metadata } }),
320
+ complete: () => {
321
+ step.status = 'completed'
322
+ step.completedAt = Date.now()
323
+ step.duration = step.completedAt - step.startedAt
324
+ },
325
+ fail: (error: Error) => {
326
+ step.status = 'failed'
327
+ step.error = error
328
+ step.completedAt = Date.now()
329
+ step.duration = step.completedAt - step.startedAt
330
+ },
331
+ addMetadata: (data: Record<string, unknown>) => {
332
+ step.metadata = { ...step.metadata, ...data }
333
+ },
334
+ to5WHEvent: () => ({
335
+ who: (step.metadata?.['actor'] as string) || 'system',
336
+ what: (step.metadata?.['action'] as string) || step.name,
337
+ when: step.startedAt,
338
+ where: contextName || 'unknown',
339
+ ...(step.metadata?.['reason'] !== undefined && { why: step.metadata['reason'] as string }),
340
+ how: {
341
+ ...(step.duration !== undefined && { duration: step.duration }),
342
+ status: step.status,
343
+ ...(step.metadata !== undefined && { metadata: step.metadata }),
344
+ },
345
+ }),
346
+ }
347
+ return step
348
+ }
349
+
350
+ /**
351
+ * Record a step in the cascade context
352
+ */
353
+ export function recordStep(
354
+ ctx: CascadeContext,
355
+ name: string,
356
+ metadata?: Record<string, unknown>
357
+ ): CascadeStep {
358
+ const startedAt = Date.now()
359
+
360
+ const step: CascadeStep = {
361
+ name,
362
+ startedAt,
363
+ status: 'running',
364
+ ...(metadata !== undefined && { metadata: { ...metadata } }),
365
+ complete: () => {
366
+ step.status = 'completed'
367
+ step.completedAt = Date.now()
368
+ step.duration = step.completedAt - step.startedAt
369
+ ctx.path.push(name)
370
+ },
371
+ fail: (error: Error) => {
372
+ step.status = 'failed'
373
+ step.error = error
374
+ step.completedAt = Date.now()
375
+ step.duration = step.completedAt - step.startedAt
376
+ },
377
+ addMetadata: (data: Record<string, unknown>) => {
378
+ step.metadata = { ...step.metadata, ...data }
379
+ },
380
+ to5WHEvent: () => ({
381
+ who: (step.metadata?.['actor'] as string) || 'system',
382
+ what: (step.metadata?.['action'] as string) || name,
383
+ when: startedAt,
384
+ where: ctx.name || 'cascade',
385
+ ...(step.metadata?.['reason'] !== undefined && { why: step.metadata['reason'] as string }),
386
+ how: {
387
+ ...(step.duration !== undefined && { duration: step.duration }),
388
+ status: step.status,
389
+ ...(step.metadata !== undefined && { metadata: step.metadata }),
390
+ },
391
+ }),
392
+ }
393
+
394
+ ctx.steps.push(step)
395
+ return step
396
+ }
397
+
398
+ /**
399
+ * Execute a callback with cascade context
400
+ */
401
+ export async function withCascadeContext<T>(
402
+ callback: (ctx: CascadeContext) => Promise<T>,
403
+ options: CascadeContextOptions = {}
404
+ ): Promise<T> {
405
+ const ctx = createCascadeContext(options)
406
+ return callback(ctx)
407
+ }
408
+
409
+ /**
410
+ * Serialize context for transmission/storage
411
+ */
412
+ function serializeContext(ctx: CascadeContext): SerializedCascadeContext {
413
+ return {
414
+ correlationId: ctx.correlationId,
415
+ spanId: ctx.spanId,
416
+ ...(ctx.parentId !== undefined && { parentId: ctx.parentId }),
417
+ ...(ctx.name !== undefined && { name: ctx.name }),
418
+ depth: ctx.depth,
419
+ steps: ctx.steps.map((step) => ({
420
+ name: step.name,
421
+ startedAt: step.startedAt,
422
+ ...(step.completedAt !== undefined && { completedAt: step.completedAt }),
423
+ ...(step.duration !== undefined && { duration: step.duration }),
424
+ status: step.status,
425
+ ...(step.metadata !== undefined && { metadata: step.metadata }),
426
+ })),
427
+ path: ctx.path,
428
+ createdAt: ctx.createdAt,
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Format context as readable string
434
+ */
435
+ function formatContext(ctx: CascadeContext): string {
436
+ const lines: string[] = []
437
+ lines.push(`Cascade: ${ctx.name || 'unnamed'}`)
438
+ lines.push(` Correlation ID: ${ctx.correlationId}`)
439
+ lines.push(` Span ID: ${ctx.spanId}`)
440
+ if (ctx.parentId) {
441
+ lines.push(` Parent ID: ${ctx.parentId}`)
442
+ }
443
+ lines.push(` Depth: ${ctx.depth}`)
444
+ lines.push(` Steps:`)
445
+ for (const step of ctx.steps) {
446
+ const status =
447
+ step.status === 'completed' ? '[OK]' : step.status === 'failed' ? '[FAIL]' : '[...]'
448
+ const duration = step.duration ? ` (${step.duration}ms)` : ''
449
+ lines.push(` ${status} ${step.name}${duration}`)
450
+ }
451
+ return lines.join('\n')
452
+ }
453
+
454
+ /**
455
+ * Format context as tree including parent contexts
456
+ */
457
+ function formatContextTree(ctx: CascadeContext): string {
458
+ const lines: string[] = []
459
+
460
+ // Build tree from root
461
+ const contexts: CascadeContext[] = []
462
+ let current: CascadeContext | undefined = ctx
463
+ while (current) {
464
+ contexts.unshift(current)
465
+ current = current.parent
466
+ }
467
+
468
+ for (let i = 0; i < contexts.length; i++) {
469
+ const context = contexts[i]!
470
+ const indent = ' '.repeat(i)
471
+ lines.push(`${indent}${context.name || 'cascade'} (depth: ${context.depth})`)
472
+ for (const step of context.steps) {
473
+ const status =
474
+ step.status === 'completed' ? '[OK]' : step.status === 'failed' ? '[FAIL]' : '[...]'
475
+ const duration = step.duration ? ` (${step.duration}ms)` : ''
476
+ lines.push(`${indent} ${status} ${step.name}${duration}`)
477
+ }
478
+ }
479
+
480
+ return lines.join('\n')
481
+ }
482
+
483
+ /**
484
+ * Convert to W3C Trace Context format
485
+ */
486
+ function toTraceContext(ctx: CascadeContext): TraceContext {
487
+ const version = '00'
488
+ const traceId = correlationIdToTraceId(ctx.correlationId)
489
+ const spanId = ctx.spanId.padEnd(16, '0').slice(0, 16)
490
+ const flags = '01' // sampled
491
+
492
+ return {
493
+ traceparent: `${version}-${traceId}-${spanId}-${flags}`,
494
+ }
495
+ }