ai-workflows 2.1.3 → 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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -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,717 @@
1
+ /**
2
+ * DurableWorkflow - Persistent workflow implementation using digital-objects
3
+ *
4
+ * Provides durable, recoverable workflows with:
5
+ * - Workflow state stored as Things
6
+ * - History and events stored as Actions
7
+ * - Automatic state recovery on restart
8
+ * - Graph-based dependency tracking
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createMemoryProvider } from 'digital-objects'
13
+ * import { DurableWorkflow } from 'ai-workflows'
14
+ *
15
+ * const provider = createMemoryProvider()
16
+ * const workflow = new DurableWorkflow(provider)
17
+ *
18
+ * await workflow.initialize('my-workflow', $ => {
19
+ * $.on.Order.created(async (order, $) => {
20
+ * await $.send('Invoice.generate', { orderId: order.id })
21
+ * })
22
+ * })
23
+ *
24
+ * await workflow.start()
25
+ * await workflow.send('Order.created', { id: 'order-1', total: 100 })
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+
31
+ import type { DigitalObjectsProvider, Thing, Action } from 'digital-objects'
32
+ import type {
33
+ WorkflowContext,
34
+ WorkflowState,
35
+ WorkflowHistoryEntry,
36
+ EventHandler,
37
+ ScheduleHandler,
38
+ EventRegistration,
39
+ ScheduleRegistration,
40
+ ScheduleInterval,
41
+ OnProxy,
42
+ EveryProxy,
43
+ EveryProxyTarget,
44
+ ParsedEvent,
45
+ } from './types.js'
46
+ import { PLURAL_UNITS, isPluralUnitKey } from './types.js'
47
+ import {
48
+ createDigitalObjectsAdapter,
49
+ type WorkflowThingData,
50
+ type DigitalObjectsDatabaseContext,
51
+ } from './digital-objects-adapter.js'
52
+ import { parseEvent } from './workflow.js'
53
+ import {
54
+ registerTimer,
55
+ clearTimersForWorkflow,
56
+ getTimerIdsForWorkflow,
57
+ registerProcessCleanup,
58
+ } from './timer-registry.js'
59
+ import { getLogger } from './logger.js'
60
+
61
+ /**
62
+ * Well-known cron patterns for common schedules
63
+ */
64
+ const KNOWN_PATTERNS: Record<string, string> = {
65
+ second: '* * * * * *',
66
+ minute: '* * * * *',
67
+ hour: '0 * * * *',
68
+ day: '0 0 * * *',
69
+ week: '0 0 * * 0',
70
+ month: '0 0 1 * *',
71
+ year: '0 0 1 1 *',
72
+ Monday: '0 0 * * 1',
73
+ Tuesday: '0 0 * * 2',
74
+ Wednesday: '0 0 * * 3',
75
+ Thursday: '0 0 * * 4',
76
+ Friday: '0 0 * * 5',
77
+ Saturday: '0 0 * * 6',
78
+ Sunday: '0 0 * * 0',
79
+ weekday: '0 0 * * 1-5',
80
+ weekend: '0 0 * * 0,6',
81
+ midnight: '0 0 * * *',
82
+ noon: '0 12 * * *',
83
+ }
84
+
85
+ /**
86
+ * Time suffixes for day-based schedules
87
+ */
88
+ const TIME_PATTERNS: Record<string, { hour: number; minute: number }> = {
89
+ at6am: { hour: 6, minute: 0 },
90
+ at7am: { hour: 7, minute: 0 },
91
+ at8am: { hour: 8, minute: 0 },
92
+ at9am: { hour: 9, minute: 0 },
93
+ at10am: { hour: 10, minute: 0 },
94
+ at11am: { hour: 11, minute: 0 },
95
+ at12pm: { hour: 12, minute: 0 },
96
+ atnoon: { hour: 12, minute: 0 },
97
+ at1pm: { hour: 13, minute: 0 },
98
+ at2pm: { hour: 14, minute: 0 },
99
+ at3pm: { hour: 15, minute: 0 },
100
+ at4pm: { hour: 16, minute: 0 },
101
+ at5pm: { hour: 17, minute: 0 },
102
+ at6pm: { hour: 18, minute: 0 },
103
+ at7pm: { hour: 19, minute: 0 },
104
+ at8pm: { hour: 20, minute: 0 },
105
+ at9pm: { hour: 21, minute: 0 },
106
+ atmidnight: { hour: 0, minute: 0 },
107
+ }
108
+
109
+ /**
110
+ * Combine a day pattern with a time pattern
111
+ */
112
+ function combineWithTime(baseCron: string, time: { hour: number; minute: number }): string {
113
+ const parts = baseCron.split(' ')
114
+ parts[0] = String(time.minute)
115
+ parts[1] = String(time.hour)
116
+ return parts.join(' ')
117
+ }
118
+
119
+ /**
120
+ * Durable workflow state stored in digital-objects
121
+ */
122
+ export interface DurableWorkflowState extends WorkflowThingData {
123
+ version: number
124
+ createdAt: number
125
+ updatedAt: number
126
+ }
127
+
128
+ /**
129
+ * History entry stored as an Action
130
+ */
131
+ export interface DurableHistoryEntry {
132
+ type: 'event' | 'schedule' | 'transition' | 'action'
133
+ name: string
134
+ data?: unknown
135
+ }
136
+
137
+ /**
138
+ * Options for DurableWorkflow
139
+ */
140
+ export interface DurableWorkflowOptions {
141
+ /**
142
+ * Existing workflow instance ID to restore
143
+ */
144
+ instanceId?: string
145
+
146
+ /**
147
+ * Auto-persist state changes immediately
148
+ * @default true
149
+ */
150
+ autoPersist?: boolean
151
+
152
+ /**
153
+ * Initial context data
154
+ */
155
+ context?: Record<string, unknown>
156
+ }
157
+
158
+ /**
159
+ * DurableWorkflow - A workflow implementation with persistent state
160
+ *
161
+ * Uses digital-objects as the backing store:
162
+ * - Workflow instance stored as a Thing
163
+ * - Events and history stored as Actions
164
+ * - State changes create audit trail
165
+ */
166
+ export class DurableWorkflow {
167
+ private provider: DigitalObjectsProvider
168
+ private db!: DigitalObjectsDatabaseContext
169
+ private instanceId: string
170
+ private autoPersist: boolean
171
+ private initialized = false
172
+
173
+ // Internal state (cached from digital-objects)
174
+ private eventRegistry: EventRegistration[] = []
175
+ private scheduleRegistry: ScheduleRegistration[] = []
176
+ private state: WorkflowState = { context: {}, history: [] }
177
+ private scheduleTimers: NodeJS.Timeout[] = []
178
+
179
+ // The $ context
180
+ private $!: WorkflowContext
181
+
182
+ /**
183
+ * Create a new DurableWorkflow
184
+ *
185
+ * @param provider - The digital-objects provider (MemoryProvider, NS, etc.)
186
+ * @param options - Configuration options
187
+ */
188
+ constructor(provider: DigitalObjectsProvider, options: DurableWorkflowOptions = {}) {
189
+ this.provider = provider
190
+ this.instanceId =
191
+ options.instanceId ?? `workflow-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`
192
+ this.autoPersist = options.autoPersist ?? true
193
+
194
+ if (options.context) {
195
+ this.state.context = { ...options.context }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Get the workflow instance ID
201
+ */
202
+ get id(): string {
203
+ return this.instanceId
204
+ }
205
+
206
+ /**
207
+ * Get the current workflow state
208
+ */
209
+ getState(): WorkflowState {
210
+ return structuredClone({
211
+ ...(this.state.current !== undefined && { current: this.state.current }),
212
+ context: this.state.context,
213
+ history: this.state.history,
214
+ })
215
+ }
216
+
217
+ /**
218
+ * Initialize the workflow
219
+ *
220
+ * Creates or restores the workflow instance and runs the setup function
221
+ * to register event and schedule handlers.
222
+ *
223
+ * @param name - Workflow name for identification
224
+ * @param setup - Setup function that registers handlers using $
225
+ */
226
+ async initialize(name: string, setup: ($: WorkflowContext) => void): Promise<void> {
227
+ if (this.initialized) {
228
+ throw new Error('Workflow already initialized')
229
+ }
230
+
231
+ // Create the database adapter
232
+ this.db = await createDigitalObjectsAdapter(this.provider, {
233
+ workflowId: this.instanceId,
234
+ })
235
+
236
+ // Check if workflow instance exists (for recovery)
237
+ const existing = await this.provider.get<DurableWorkflowState>(this.instanceId)
238
+
239
+ if (existing) {
240
+ // Restore state from existing workflow
241
+ this.state.context = existing.data.context
242
+ getLogger().log(`[durable-workflow] Restored workflow ${this.instanceId}`)
243
+
244
+ // Load history from actions
245
+ const actions = await this.db.listWorkflowActions<DurableHistoryEntry>(this.instanceId)
246
+ this.state.history = actions.map((a) => ({
247
+ timestamp: a.createdAt.getTime(),
248
+ type: a.data?.type ?? 'action',
249
+ name: a.data?.name ?? a.verb,
250
+ data: a.data?.data,
251
+ }))
252
+ } else {
253
+ // Create new workflow instance
254
+ await this.provider.create<DurableWorkflowState>(
255
+ 'Workflow',
256
+ {
257
+ name,
258
+ status: 'initializing',
259
+ context: this.state.context,
260
+ registeredEvents: [],
261
+ registeredSchedules: [],
262
+ version: 1,
263
+ createdAt: Date.now(),
264
+ updatedAt: Date.now(),
265
+ },
266
+ this.instanceId
267
+ )
268
+
269
+ getLogger().log(`[durable-workflow] Created workflow ${this.instanceId}`)
270
+ }
271
+
272
+ // Create the $ context
273
+ this.$ = this.createContext()
274
+
275
+ // Run setup to capture handlers
276
+ setup(this.$)
277
+
278
+ // Update workflow with registered handlers
279
+ await this.provider.update<DurableWorkflowState>(this.instanceId, {
280
+ status: 'running',
281
+ registeredEvents: this.eventRegistry.map((e) => `${e.noun}.${e.event}`),
282
+ registeredSchedules: this.scheduleRegistry.map((s) =>
283
+ s.interval.type === 'natural'
284
+ ? s.interval.description
285
+ : s.interval.type === 'cron'
286
+ ? s.interval.expression
287
+ : `${s.interval.type}:${s.interval.value ?? 1}`
288
+ ),
289
+ updatedAt: Date.now(),
290
+ })
291
+
292
+ this.initialized = true
293
+ }
294
+
295
+ /**
296
+ * Start the workflow (begin processing schedules)
297
+ */
298
+ async start(): Promise<void> {
299
+ if (!this.initialized) {
300
+ throw new Error('Workflow not initialized. Call initialize() first.')
301
+ }
302
+
303
+ getLogger().log(
304
+ `[durable-workflow] Starting with ${this.eventRegistry.length} event handlers and ${this.scheduleRegistry.length} schedules`
305
+ )
306
+
307
+ // Register process cleanup
308
+ registerProcessCleanup()
309
+
310
+ // Start schedules
311
+ for (const schedule of this.scheduleRegistry) {
312
+ const { interval, handler } = schedule
313
+
314
+ let ms = 0
315
+ switch (interval.type) {
316
+ case 'second':
317
+ ms = (interval.value ?? 1) * 1000
318
+ break
319
+ case 'minute':
320
+ ms = (interval.value ?? 1) * 60 * 1000
321
+ break
322
+ case 'hour':
323
+ ms = (interval.value ?? 1) * 60 * 60 * 1000
324
+ break
325
+ case 'day':
326
+ ms = (interval.value ?? 1) * 24 * 60 * 60 * 1000
327
+ break
328
+ case 'week':
329
+ ms = (interval.value ?? 1) * 7 * 24 * 60 * 60 * 1000
330
+ break
331
+ case 'cron':
332
+ case 'natural':
333
+ throw new Error(
334
+ `Cron scheduling not yet implemented: "${
335
+ interval.type === 'cron' ? interval.expression : interval.description
336
+ }". Use interval-based patterns instead.`
337
+ )
338
+ }
339
+
340
+ if (ms > 0) {
341
+ const timer = setInterval(async () => {
342
+ try {
343
+ await this.addHistory('schedule', interval.natural ?? interval.type)
344
+ await handler(this.$)
345
+ } catch (error) {
346
+ getLogger().error('[durable-workflow] Schedule handler error:', error)
347
+ }
348
+ }, ms)
349
+ this.scheduleTimers.push(timer)
350
+ registerTimer(this.instanceId, timer)
351
+ }
352
+ }
353
+
354
+ // Record start action
355
+ await this.addHistory('transition', 'workflow.started')
356
+ }
357
+
358
+ /**
359
+ * Stop the workflow
360
+ */
361
+ async stop(): Promise<void> {
362
+ getLogger().log('[durable-workflow] Stopping')
363
+ this.cleanup()
364
+
365
+ await this.provider.update<DurableWorkflowState>(this.instanceId, {
366
+ status: 'paused',
367
+ updatedAt: Date.now(),
368
+ })
369
+
370
+ await this.addHistory('transition', 'workflow.stopped')
371
+ }
372
+
373
+ /**
374
+ * Send an event to the workflow
375
+ */
376
+ async send<T = unknown>(event: string, data: T): Promise<string> {
377
+ if (!this.initialized) {
378
+ throw new Error('Workflow not initialized')
379
+ }
380
+
381
+ const eventId = this.$.send(event, data)
382
+
383
+ // Deliver to handlers
384
+ await this.deliverEvent(event, { ...(data as object), _eventId: eventId })
385
+
386
+ return eventId
387
+ }
388
+
389
+ /**
390
+ * Destroy the workflow and clean up all resources
391
+ */
392
+ async destroy(): Promise<void> {
393
+ this.cleanup()
394
+
395
+ await this.provider.update<DurableWorkflowState>(this.instanceId, {
396
+ status: 'completed',
397
+ updatedAt: Date.now(),
398
+ })
399
+ }
400
+
401
+ /**
402
+ * Get the number of active timers
403
+ */
404
+ get timerCount(): number {
405
+ return getTimerIdsForWorkflow(this.instanceId).length
406
+ }
407
+
408
+ /**
409
+ * Get timer IDs for this workflow
410
+ */
411
+ getTimerIds(): string[] {
412
+ return getTimerIdsForWorkflow(this.instanceId)
413
+ }
414
+
415
+ // ==========================================================================
416
+ // Private methods
417
+ // ==========================================================================
418
+
419
+ /**
420
+ * Create the $ context
421
+ */
422
+ private createContext(): WorkflowContext {
423
+ const self = this
424
+
425
+ return {
426
+ track(event: string, data: unknown): void {
427
+ try {
428
+ self.addHistory('event', `track:${event}`, data).catch(() => {})
429
+ self.deliverEvent(event, data).catch(() => {})
430
+ } catch {
431
+ // Silently swallow errors
432
+ }
433
+ },
434
+
435
+ send<T = unknown>(event: string, data: T): string {
436
+ const eventId = crypto.randomUUID()
437
+ self.addHistory('event', event, data).catch((err) => {
438
+ getLogger().error(`[durable-workflow] Failed to record event ${event}:`, err)
439
+ })
440
+
441
+ // Record to database (durable)
442
+ self.db.recordEvent(event, { ...(data as object), _eventId: eventId }).catch((err) => {
443
+ getLogger().error(`[durable-workflow] Failed to persist event ${event}:`, err)
444
+ })
445
+
446
+ return eventId
447
+ },
448
+
449
+ async do<TData = unknown, TResult = unknown>(event: string, data: TData): Promise<TResult> {
450
+ await self.addHistory('action', `do:${event}`, data)
451
+ await self.db.recordEvent(event, data)
452
+ return self.executeEvent<TResult>(event, data, true)
453
+ },
454
+
455
+ async try<TData = unknown, TResult = unknown>(event: string, data: TData): Promise<TResult> {
456
+ await self.addHistory('action', `try:${event}`, data)
457
+ return self.executeEvent<TResult>(event, data, false)
458
+ },
459
+
460
+ on: self.createOnProxy(),
461
+ every: self.createEveryProxy(),
462
+
463
+ state: self.state.context,
464
+
465
+ getState(): WorkflowState {
466
+ return self.getState()
467
+ },
468
+
469
+ set<T = unknown>(key: string, value: T): void {
470
+ self.state.context[key] = value
471
+ if (self.autoPersist) {
472
+ self.persistState().catch((err) => {
473
+ getLogger().error(`[durable-workflow] Failed to persist state:`, err)
474
+ })
475
+ }
476
+ },
477
+
478
+ get<T = unknown>(key: string): T | undefined {
479
+ return self.state.context[key] as T | undefined
480
+ },
481
+
482
+ log(message: string, data?: unknown): void {
483
+ self.addHistory('action', 'log', { message, data }).catch(() => {})
484
+ getLogger().log(`[durable-workflow] ${message}`, data ?? '')
485
+ },
486
+
487
+ db: self.db,
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Create the $.on proxy
493
+ */
494
+ private createOnProxy(): OnProxy {
495
+ const self = this
496
+ return new Proxy({} as OnProxy, {
497
+ get(_target, noun: string) {
498
+ return new Proxy(
499
+ {},
500
+ {
501
+ get(_eventTarget, event: string) {
502
+ return (handler: EventHandler) => {
503
+ self.eventRegistry.push({
504
+ noun,
505
+ event,
506
+ handler,
507
+ source: handler.toString(),
508
+ })
509
+ }
510
+ },
511
+ }
512
+ )
513
+ },
514
+ })
515
+ }
516
+
517
+ /**
518
+ * Create the $.every proxy
519
+ */
520
+ private createEveryProxy(): EveryProxy {
521
+ const self = this
522
+
523
+ const handler = {
524
+ get(_target: unknown, prop: string) {
525
+ const pattern = KNOWN_PATTERNS[prop]
526
+ if (pattern) {
527
+ const result = (handlerFn: ScheduleHandler) => {
528
+ self.scheduleRegistry.push({
529
+ interval: { type: 'cron', expression: pattern, natural: prop },
530
+ handler: handlerFn,
531
+ source: handlerFn.toString(),
532
+ })
533
+ }
534
+ return new Proxy(result, {
535
+ get(_t, timeKey: string) {
536
+ const time = TIME_PATTERNS[timeKey]
537
+ if (time) {
538
+ const cron = combineWithTime(pattern, time)
539
+ return (handlerFn: ScheduleHandler) => {
540
+ self.scheduleRegistry.push({
541
+ interval: { type: 'cron', expression: cron, natural: `${prop}.${timeKey}` },
542
+ handler: handlerFn,
543
+ source: handlerFn.toString(),
544
+ })
545
+ }
546
+ }
547
+ return undefined
548
+ },
549
+ apply(_t, _thisArg, args) {
550
+ self.scheduleRegistry.push({
551
+ interval: { type: 'cron', expression: pattern, natural: prop },
552
+ handler: args[0],
553
+ source: args[0].toString(),
554
+ })
555
+ },
556
+ })
557
+ }
558
+
559
+ // Plural units (seconds, minutes, hours, days, weeks)
560
+ if (isPluralUnitKey(prop)) {
561
+ const intervalType = PLURAL_UNITS[prop]
562
+ return (value: number) => (handlerFn: ScheduleHandler) => {
563
+ self.scheduleRegistry.push({
564
+ interval: { type: intervalType, value, natural: `${value} ${prop}` },
565
+ handler: handlerFn,
566
+ source: handlerFn.toString(),
567
+ })
568
+ }
569
+ }
570
+
571
+ return undefined
572
+ },
573
+
574
+ apply(_target: unknown, _thisArg: unknown, args: unknown[]) {
575
+ const [description, handlerFn] = args as [string, ScheduleHandler]
576
+ if (typeof description === 'string' && typeof handlerFn === 'function') {
577
+ self.scheduleRegistry.push({
578
+ interval: { type: 'natural', description },
579
+ handler: handlerFn,
580
+ source: handlerFn.toString(),
581
+ })
582
+ }
583
+ },
584
+ }
585
+
586
+ const target: EveryProxyTarget = function (_description: string, _handler: ScheduleHandler) {}
587
+ return new Proxy(target, handler) as unknown as EveryProxy
588
+ }
589
+
590
+ /**
591
+ * Add history entry (persisted as Action)
592
+ */
593
+ private async addHistory(
594
+ type: WorkflowHistoryEntry['type'],
595
+ name: string,
596
+ data?: unknown
597
+ ): Promise<void> {
598
+ const entry: WorkflowHistoryEntry = {
599
+ timestamp: Date.now(),
600
+ type,
601
+ name,
602
+ data,
603
+ }
604
+ this.state.history.push(entry)
605
+
606
+ // Persist as Action
607
+ await this.provider.perform<DurableHistoryEntry>(type, this.instanceId, undefined, {
608
+ type,
609
+ name,
610
+ data,
611
+ })
612
+ }
613
+
614
+ /**
615
+ * Persist current state to digital-objects
616
+ */
617
+ private async persistState(): Promise<void> {
618
+ await this.provider.update<DurableWorkflowState>(this.instanceId, {
619
+ context: this.state.context,
620
+ updatedAt: Date.now(),
621
+ })
622
+ }
623
+
624
+ /**
625
+ * Deliver an event to matching handlers
626
+ */
627
+ private async deliverEvent(event: string, data: unknown): Promise<void> {
628
+ const parsed = parseEvent(event)
629
+ if (!parsed) {
630
+ getLogger().warn(`Invalid event format: ${event}. Expected Noun.event`)
631
+ return
632
+ }
633
+
634
+ const matching = this.eventRegistry.filter(
635
+ (h) => h.noun === parsed.noun && h.event === parsed.event
636
+ )
637
+
638
+ if (matching.length === 0) {
639
+ return
640
+ }
641
+
642
+ await Promise.all(
643
+ matching.map(async ({ handler }) => {
644
+ try {
645
+ await handler(data, this.$)
646
+ } catch (error) {
647
+ getLogger().error(`Error in handler for ${event}:`, error)
648
+ }
649
+ })
650
+ )
651
+ }
652
+
653
+ /**
654
+ * Execute an event and wait for result
655
+ */
656
+ private async executeEvent<TResult>(
657
+ event: string,
658
+ data: unknown,
659
+ durable: boolean
660
+ ): Promise<TResult> {
661
+ const parsed = parseEvent(event)
662
+ if (!parsed) {
663
+ throw new Error(`Invalid event format: ${event}. Expected Noun.event`)
664
+ }
665
+
666
+ const matching = this.eventRegistry.filter(
667
+ (h) => h.noun === parsed.noun && h.event === parsed.event
668
+ )
669
+
670
+ if (matching.length === 0) {
671
+ throw new Error(`No handler registered for ${event}`)
672
+ }
673
+
674
+ const { handler } = matching[0]!
675
+
676
+ if (durable) {
677
+ await this.db.createAction({
678
+ actor: 'workflow',
679
+ object: event,
680
+ action: 'execute',
681
+ metadata: { data },
682
+ })
683
+ }
684
+
685
+ try {
686
+ const result = await handler(data, this.$)
687
+ return result as TResult
688
+ } catch (error) {
689
+ if (durable) {
690
+ getLogger().error(`[durable-workflow] Durable action failed for ${event}:`, error)
691
+ }
692
+ throw error
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Clean up timers
698
+ */
699
+ private cleanup(): void {
700
+ clearTimersForWorkflow(this.instanceId)
701
+ this.scheduleTimers = []
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Factory function to create a DurableWorkflow
707
+ *
708
+ * @param provider - The digital-objects provider
709
+ * @param options - Configuration options
710
+ * @returns A new DurableWorkflow instance
711
+ */
712
+ export function createDurableWorkflow(
713
+ provider: DigitalObjectsProvider,
714
+ options?: DurableWorkflowOptions
715
+ ): DurableWorkflow {
716
+ return new DurableWorkflow(provider, options)
717
+ }