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,658 @@
1
+ /**
2
+ * DatabaseContext Implementation - Persistence layer for ai-workflows using ai-database
3
+ *
4
+ * Provides durable storage for workflow events, actions, and artifacts with:
5
+ * - Event sourcing (immutable event log)
6
+ * - State snapshots and restoration
7
+ * - Event replay capabilities
8
+ * - Action tracking with status management
9
+ * - Artifact caching for compiled content
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { DB } from 'ai-database'
14
+ * import { createDatabaseContext } from 'ai-workflows'
15
+ *
16
+ * const { db, events } = DB({
17
+ * WorkflowEvent: { type: 'string', data: 'string' },
18
+ * WorkflowAction: { actor: 'string', status: 'string' },
19
+ * WorkflowArtifact: { key: 'string', type: 'string' },
20
+ * WorkflowSnapshot: { workflowId: 'string', state: 'string' },
21
+ * })
22
+ *
23
+ * const dbContext = createDatabaseContext({ db, events })
24
+ *
25
+ * // Use with Workflow
26
+ * const workflow = Workflow($ => {
27
+ * $.on.Customer.created(async (customer, $) => {
28
+ * // Events are now persisted durably
29
+ * $.send('Email.welcome', { to: customer.email })
30
+ * })
31
+ * }, { db: dbContext })
32
+ * ```
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+
37
+ import type { DatabaseContext, ActionData, ArtifactData } from './types.js'
38
+ import { getLogger } from './logger.js'
39
+
40
+ /**
41
+ * Event stored in the database
42
+ */
43
+ export interface StoredEvent {
44
+ $id: string
45
+ $type: string
46
+ eventType: string
47
+ data: string // JSON serialized
48
+ timestamp: number
49
+ correlationId?: string
50
+ causationId?: string
51
+ workflowId?: string
52
+ source?: string
53
+ }
54
+
55
+ /**
56
+ * Action stored in the database
57
+ */
58
+ export interface StoredAction {
59
+ $id: string
60
+ $type: string
61
+ actor: string
62
+ object: string
63
+ action: string
64
+ status: 'pending' | 'active' | 'completed' | 'failed'
65
+ metadata: string // JSON serialized
66
+ result?: string // JSON serialized
67
+ createdAt: number
68
+ updatedAt: number
69
+ completedAt?: number
70
+ }
71
+
72
+ /**
73
+ * Artifact stored in the database
74
+ */
75
+ export interface StoredArtifact {
76
+ $id: string
77
+ $type: string
78
+ key: string
79
+ artifactType: string
80
+ sourceHash: string
81
+ content: string // JSON serialized
82
+ metadata?: string // JSON serialized
83
+ createdAt: number
84
+ }
85
+
86
+ /**
87
+ * Snapshot stored in the database
88
+ */
89
+ export interface StoredSnapshot {
90
+ $id: string
91
+ $type: string
92
+ workflowId: string
93
+ label?: string
94
+ state: string // JSON serialized
95
+ eventSequence: number // Last event sequence at snapshot time
96
+ createdAt: number
97
+ }
98
+
99
+ /**
100
+ * Database provider interface for persistence operations
101
+ * Compatible with ai-database DB() result or any similar provider
102
+ */
103
+ export interface DatabaseProvider {
104
+ get: (type: string, id: string) => Promise<Record<string, unknown> | null>
105
+ create: (
106
+ type: string,
107
+ data: Record<string, unknown>,
108
+ id?: string
109
+ ) => Promise<Record<string, unknown>>
110
+ update: (
111
+ type: string,
112
+ id: string,
113
+ data: Record<string, unknown>
114
+ ) => Promise<Record<string, unknown>>
115
+ delete: (type: string, id: string) => Promise<boolean>
116
+ list: (
117
+ type: string,
118
+ options?: { limit?: number; offset?: number; where?: Record<string, unknown> }
119
+ ) => Promise<Record<string, unknown>[]>
120
+ emit?: (event: string, data: unknown) => Promise<{ id: string }>
121
+ }
122
+
123
+ /**
124
+ * Events API interface for event subscription
125
+ */
126
+ export interface EventsAPI {
127
+ on: (event: string, handler: (data: unknown) => void) => () => void
128
+ emit: (
129
+ event: string | { event: string; [key: string]: unknown },
130
+ data?: unknown
131
+ ) => Promise<{ id: string }>
132
+ list?: (options?: { event?: string; since?: Date; limit?: number }) => Promise<unknown[]>
133
+ replay?: (options: {
134
+ event?: string
135
+ since?: Date
136
+ handler: (event: unknown) => Promise<void>
137
+ }) => Promise<void>
138
+ }
139
+
140
+ /**
141
+ * Options for creating a DatabaseContext
142
+ */
143
+ export interface DatabaseContextOptions {
144
+ /** Database provider (from ai-database DB() or compatible) */
145
+ db: DatabaseProvider
146
+ /** Events API (optional, for event sourcing) */
147
+ events?: EventsAPI
148
+ /** Workflow ID for scoping events */
149
+ workflowId?: string
150
+ /** Source identifier for events */
151
+ source?: string
152
+ }
153
+
154
+ /**
155
+ * Extended DatabaseContext with event sourcing capabilities
156
+ */
157
+ export interface EventSourcingContext extends DatabaseContext {
158
+ /** Get all events for a workflow */
159
+ getEvents: (options?: { since?: Date; limit?: number }) => Promise<StoredEvent[]>
160
+
161
+ /** Replay events through a handler */
162
+ replay: (
163
+ handler: (event: string, data: unknown) => Promise<void>,
164
+ options?: { since?: Date }
165
+ ) => Promise<void>
166
+
167
+ /** Create a snapshot of current state */
168
+ createSnapshot: (state: unknown, label?: string) => Promise<string>
169
+
170
+ /** Restore state from a snapshot */
171
+ restoreSnapshot: (snapshotId: string) => Promise<unknown>
172
+
173
+ /** Get available snapshots */
174
+ getSnapshots: () => Promise<Array<{ id: string; label?: string; createdAt: Date }>>
175
+
176
+ /** Get the event sequence number */
177
+ getEventSequence: () => number
178
+ }
179
+
180
+ /**
181
+ * Entity type names for database storage
182
+ */
183
+ const ENTITY_TYPES = {
184
+ EVENT: 'WorkflowEvent',
185
+ ACTION: 'WorkflowAction',
186
+ ARTIFACT: 'WorkflowArtifact',
187
+ SNAPSHOT: 'WorkflowSnapshot',
188
+ } as const
189
+
190
+ /**
191
+ * Generate a unique ID
192
+ */
193
+ function generateId(): string {
194
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
195
+ return crypto.randomUUID()
196
+ }
197
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
198
+ }
199
+
200
+ /**
201
+ * Create a DatabaseContext implementation backed by ai-database
202
+ *
203
+ * @param options - Configuration options
204
+ * @returns DatabaseContext implementation with event sourcing capabilities
205
+ */
206
+ export function createDatabaseContext(options: DatabaseContextOptions): EventSourcingContext {
207
+ const { db, events, workflowId, source = 'workflow' } = options
208
+
209
+ // Track event sequence for ordering
210
+ let eventSequence = 0
211
+
212
+ // Event handlers for live subscriptions
213
+ const eventHandlers = new Map<string, Set<(data: unknown) => void>>()
214
+
215
+ /**
216
+ * Subscribe to live events from ai-database
217
+ */
218
+ if (events?.on) {
219
+ // Subscribe to workflow events
220
+ events.on('WorkflowEvent.created', (data) => {
221
+ const event = data as StoredEvent
222
+ const handlers = eventHandlers.get(event.eventType)
223
+ if (handlers) {
224
+ const parsedData = JSON.parse(event.data)
225
+ for (const handler of handlers) {
226
+ try {
227
+ handler(parsedData)
228
+ } catch (error) {
229
+ getLogger().error(
230
+ `[database-context] Error in event handler for ${event.eventType}:`,
231
+ error
232
+ )
233
+ }
234
+ }
235
+ }
236
+ })
237
+ }
238
+
239
+ return {
240
+ /**
241
+ * Record an event (immutable)
242
+ */
243
+ async recordEvent(eventType: string, data: unknown): Promise<void> {
244
+ eventSequence++
245
+ const eventId = generateId()
246
+ const timestamp = Date.now()
247
+
248
+ const storedEvent: Omit<StoredEvent, '$id' | '$type'> = {
249
+ eventType,
250
+ data: JSON.stringify(data),
251
+ timestamp,
252
+ source,
253
+ ...(workflowId && { workflowId }),
254
+ }
255
+
256
+ // Store in database
257
+ await db.create(
258
+ ENTITY_TYPES.EVENT,
259
+ storedEvent as unknown as Record<string, unknown>,
260
+ eventId
261
+ )
262
+
263
+ // Emit to events API if available
264
+ if (events?.emit) {
265
+ await events.emit({
266
+ event: 'WorkflowEvent.created',
267
+ actor: source,
268
+ object: `${ENTITY_TYPES.EVENT}/${eventId}`,
269
+ objectData: storedEvent,
270
+ })
271
+ }
272
+ },
273
+
274
+ /**
275
+ * Create an action (pending work)
276
+ */
277
+ async createAction(action: ActionData): Promise<void> {
278
+ const actionId = generateId()
279
+ const now = Date.now()
280
+
281
+ const storedAction: Omit<StoredAction, '$id' | '$type'> = {
282
+ actor: action.actor,
283
+ object: action.object,
284
+ action: action.action,
285
+ status: action.status ?? 'pending',
286
+ metadata: JSON.stringify(action.metadata ?? {}),
287
+ createdAt: now,
288
+ updatedAt: now,
289
+ }
290
+
291
+ await db.create(
292
+ ENTITY_TYPES.ACTION,
293
+ storedAction as unknown as Record<string, unknown>,
294
+ actionId
295
+ )
296
+
297
+ if (events?.emit) {
298
+ await events.emit({
299
+ event: 'WorkflowAction.created',
300
+ actor: action.actor,
301
+ object: `${ENTITY_TYPES.ACTION}/${actionId}`,
302
+ objectData: storedAction,
303
+ })
304
+ }
305
+ },
306
+
307
+ /**
308
+ * Complete an action
309
+ */
310
+ async completeAction(id: string, result: unknown): Promise<void> {
311
+ const existing = await db.get(ENTITY_TYPES.ACTION, id)
312
+ if (!existing) {
313
+ throw new Error(`Action not found: ${id}`)
314
+ }
315
+
316
+ const now = Date.now()
317
+ await db.update(ENTITY_TYPES.ACTION, id, {
318
+ status: 'completed',
319
+ result: JSON.stringify(result),
320
+ updatedAt: now,
321
+ completedAt: now,
322
+ })
323
+
324
+ if (events?.emit) {
325
+ // Cast is safe - we validated existing is not null above
326
+ const existingAction = existing as unknown as StoredAction
327
+ await events.emit({
328
+ event: 'WorkflowAction.completed',
329
+ actor: existingAction.actor,
330
+ object: `${ENTITY_TYPES.ACTION}/${id}`,
331
+ result: result,
332
+ })
333
+ }
334
+ },
335
+
336
+ /**
337
+ * Store an artifact
338
+ */
339
+ async storeArtifact(artifact: ArtifactData): Promise<void> {
340
+ const storedArtifact: Omit<StoredArtifact, '$id' | '$type'> = {
341
+ key: artifact.key,
342
+ artifactType: artifact.type,
343
+ sourceHash: artifact.sourceHash,
344
+ content: JSON.stringify(artifact.content),
345
+ ...(artifact.metadata && { metadata: JSON.stringify(artifact.metadata) }),
346
+ createdAt: Date.now(),
347
+ }
348
+
349
+ // Use key as ID for easy lookup
350
+ await db.create(
351
+ ENTITY_TYPES.ARTIFACT,
352
+ storedArtifact as unknown as Record<string, unknown>,
353
+ artifact.key
354
+ )
355
+ },
356
+
357
+ /**
358
+ * Get an artifact
359
+ */
360
+ async getArtifact(key: string): Promise<unknown | null> {
361
+ const stored = await db.get(ENTITY_TYPES.ARTIFACT, key)
362
+ if (!stored) {
363
+ return null
364
+ }
365
+
366
+ const artifact = stored as unknown as StoredArtifact
367
+ return JSON.parse(artifact.content)
368
+ },
369
+
370
+ /**
371
+ * Get all events for a workflow
372
+ */
373
+ async getEvents(queryOptions?: { since?: Date; limit?: number }): Promise<StoredEvent[]> {
374
+ const where: Record<string, unknown> = {}
375
+ if (workflowId) {
376
+ where['workflowId'] = workflowId
377
+ }
378
+
379
+ const listOptions: { limit?: number; where?: Record<string, unknown> } = {}
380
+ if (Object.keys(where).length > 0) {
381
+ listOptions.where = where
382
+ }
383
+ if (queryOptions?.limit !== undefined) {
384
+ listOptions.limit = queryOptions.limit
385
+ }
386
+
387
+ const results = await db.list(ENTITY_TYPES.EVENT, listOptions)
388
+
389
+ // Filter by timestamp if since is provided
390
+ let events = results as unknown as StoredEvent[]
391
+ if (queryOptions?.since) {
392
+ const sinceTimestamp = queryOptions.since.getTime()
393
+ events = events.filter((e) => e.timestamp >= sinceTimestamp)
394
+ }
395
+
396
+ // Sort by timestamp
397
+ events.sort((a, b) => a.timestamp - b.timestamp)
398
+
399
+ return events
400
+ },
401
+
402
+ /**
403
+ * Replay events through a handler
404
+ */
405
+ async replay(
406
+ handler: (event: string, data: unknown) => Promise<void>,
407
+ replayOptions?: { since?: Date }
408
+ ): Promise<void> {
409
+ const storedEvents = await this.getEvents(replayOptions)
410
+
411
+ for (const event of storedEvents) {
412
+ const data = JSON.parse(event.data)
413
+ await handler(event.eventType, data)
414
+ }
415
+ },
416
+
417
+ /**
418
+ * Create a snapshot of current state
419
+ */
420
+ async createSnapshot(state: unknown, label?: string): Promise<string> {
421
+ const snapshotId = `snap-${workflowId ?? 'global'}-${Date.now()}-${Math.random()
422
+ .toString(36)
423
+ .slice(2, 8)}`
424
+
425
+ const storedSnapshot: Omit<StoredSnapshot, '$id' | '$type'> = {
426
+ workflowId: workflowId ?? 'global',
427
+ ...(label && { label }),
428
+ state: JSON.stringify(state),
429
+ eventSequence,
430
+ createdAt: Date.now(),
431
+ }
432
+
433
+ await db.create(
434
+ ENTITY_TYPES.SNAPSHOT,
435
+ storedSnapshot as unknown as Record<string, unknown>,
436
+ snapshotId
437
+ )
438
+
439
+ if (events?.emit) {
440
+ await events.emit({
441
+ event: 'WorkflowSnapshot.created',
442
+ actor: source,
443
+ object: `${ENTITY_TYPES.SNAPSHOT}/${snapshotId}`,
444
+ objectData: { workflowId, label, eventSequence },
445
+ })
446
+ }
447
+
448
+ return snapshotId
449
+ },
450
+
451
+ /**
452
+ * Restore state from a snapshot
453
+ */
454
+ async restoreSnapshot(snapshotId: string): Promise<unknown> {
455
+ const stored = await db.get(ENTITY_TYPES.SNAPSHOT, snapshotId)
456
+ if (!stored) {
457
+ throw new Error(`Snapshot not found: ${snapshotId}`)
458
+ }
459
+
460
+ const snapshot = stored as unknown as StoredSnapshot
461
+
462
+ // Verify workflow ownership
463
+ if (workflowId && snapshot.workflowId !== workflowId) {
464
+ throw new Error(`Snapshot "${snapshotId}" does not belong to workflow "${workflowId}"`)
465
+ }
466
+
467
+ // Update event sequence to match snapshot
468
+ eventSequence = snapshot.eventSequence
469
+
470
+ if (events?.emit) {
471
+ await events.emit({
472
+ event: 'WorkflowSnapshot.restored',
473
+ actor: source,
474
+ object: `${ENTITY_TYPES.SNAPSHOT}/${snapshotId}`,
475
+ result: { workflowId, eventSequence },
476
+ })
477
+ }
478
+
479
+ return JSON.parse(snapshot.state)
480
+ },
481
+
482
+ /**
483
+ * Get available snapshots
484
+ */
485
+ async getSnapshots(): Promise<Array<{ id: string; label?: string; createdAt: Date }>> {
486
+ const where: Record<string, unknown> = {}
487
+ if (workflowId) {
488
+ where['workflowId'] = workflowId
489
+ }
490
+
491
+ const listOptions: { where?: Record<string, unknown> } = {}
492
+ if (Object.keys(where).length > 0) {
493
+ listOptions.where = where
494
+ }
495
+
496
+ const results = await db.list(ENTITY_TYPES.SNAPSHOT, listOptions)
497
+
498
+ return (results as unknown as StoredSnapshot[])
499
+ .map((s) => ({
500
+ id: s.$id,
501
+ ...(s.label && { label: s.label }),
502
+ createdAt: new Date(s.createdAt),
503
+ }))
504
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
505
+ },
506
+
507
+ /**
508
+ * Get the current event sequence number
509
+ */
510
+ getEventSequence(): number {
511
+ return eventSequence
512
+ },
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Create a simple in-memory DatabaseContext for testing
518
+ *
519
+ * @returns DatabaseContext implementation backed by in-memory storage
520
+ */
521
+ export function createMemoryDatabaseContext(): EventSourcingContext {
522
+ const events: StoredEvent[] = []
523
+ const actions = new Map<string, StoredAction>()
524
+ const artifacts = new Map<string, StoredArtifact>()
525
+ const snapshots = new Map<string, StoredSnapshot>()
526
+ let eventSequence = 0
527
+
528
+ return {
529
+ async recordEvent(eventType: string, data: unknown): Promise<void> {
530
+ eventSequence++
531
+ events.push({
532
+ $id: generateId(),
533
+ $type: ENTITY_TYPES.EVENT,
534
+ eventType,
535
+ data: JSON.stringify(data),
536
+ timestamp: Date.now(),
537
+ source: 'memory',
538
+ })
539
+ },
540
+
541
+ async createAction(action: ActionData): Promise<void> {
542
+ const actionId = generateId()
543
+ const now = Date.now()
544
+ actions.set(actionId, {
545
+ $id: actionId,
546
+ $type: ENTITY_TYPES.ACTION,
547
+ actor: action.actor,
548
+ object: action.object,
549
+ action: action.action,
550
+ status: action.status ?? 'pending',
551
+ metadata: JSON.stringify(action.metadata ?? {}),
552
+ createdAt: now,
553
+ updatedAt: now,
554
+ })
555
+ },
556
+
557
+ async completeAction(id: string, result: unknown): Promise<void> {
558
+ const existing = actions.get(id)
559
+ if (!existing) {
560
+ throw new Error(`Action not found: ${id}`)
561
+ }
562
+ existing.status = 'completed'
563
+ existing.result = JSON.stringify(result)
564
+ existing.updatedAt = Date.now()
565
+ existing.completedAt = Date.now()
566
+ },
567
+
568
+ async storeArtifact(artifact: ArtifactData): Promise<void> {
569
+ artifacts.set(artifact.key, {
570
+ $id: artifact.key,
571
+ $type: ENTITY_TYPES.ARTIFACT,
572
+ key: artifact.key,
573
+ artifactType: artifact.type,
574
+ sourceHash: artifact.sourceHash,
575
+ content: JSON.stringify(artifact.content),
576
+ ...(artifact.metadata && { metadata: JSON.stringify(artifact.metadata) }),
577
+ createdAt: Date.now(),
578
+ })
579
+ },
580
+
581
+ async getArtifact(key: string): Promise<unknown | null> {
582
+ const stored = artifacts.get(key)
583
+ if (!stored) {
584
+ return null
585
+ }
586
+ return JSON.parse(stored.content)
587
+ },
588
+
589
+ async getEvents(queryOptions?: { since?: Date; limit?: number }): Promise<StoredEvent[]> {
590
+ let result = [...events]
591
+
592
+ if (queryOptions?.since) {
593
+ const sinceTimestamp = queryOptions.since.getTime()
594
+ result = result.filter((e) => e.timestamp >= sinceTimestamp)
595
+ }
596
+
597
+ result.sort((a, b) => a.timestamp - b.timestamp)
598
+
599
+ if (queryOptions?.limit) {
600
+ result = result.slice(0, queryOptions.limit)
601
+ }
602
+
603
+ return result
604
+ },
605
+
606
+ async replay(
607
+ handler: (event: string, data: unknown) => Promise<void>,
608
+ replayOptions?: { since?: Date }
609
+ ): Promise<void> {
610
+ const storedEvents = await this.getEvents(replayOptions)
611
+
612
+ for (const event of storedEvents) {
613
+ const data = JSON.parse(event.data)
614
+ await handler(event.eventType, data)
615
+ }
616
+ },
617
+
618
+ async createSnapshot(state: unknown, label?: string): Promise<string> {
619
+ const snapshotId = `snap-memory-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
620
+
621
+ snapshots.set(snapshotId, {
622
+ $id: snapshotId,
623
+ $type: ENTITY_TYPES.SNAPSHOT,
624
+ workflowId: 'memory',
625
+ ...(label && { label }),
626
+ state: JSON.stringify(state),
627
+ eventSequence,
628
+ createdAt: Date.now(),
629
+ })
630
+
631
+ return snapshotId
632
+ },
633
+
634
+ async restoreSnapshot(snapshotId: string): Promise<unknown> {
635
+ const snapshot = snapshots.get(snapshotId)
636
+ if (!snapshot) {
637
+ throw new Error(`Snapshot not found: ${snapshotId}`)
638
+ }
639
+
640
+ eventSequence = snapshot.eventSequence
641
+ return JSON.parse(snapshot.state)
642
+ },
643
+
644
+ async getSnapshots(): Promise<Array<{ id: string; label?: string; createdAt: Date }>> {
645
+ return Array.from(snapshots.values())
646
+ .map((s) => ({
647
+ id: s.$id,
648
+ ...(s.label && { label: s.label }),
649
+ createdAt: new Date(s.createdAt),
650
+ }))
651
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
652
+ },
653
+
654
+ getEventSequence(): number {
655
+ return eventSequence
656
+ },
657
+ }
658
+ }