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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -1
- package/README.md +305 -184
- package/dist/barrier.d.ts +159 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +377 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -8
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +53 -19
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +77 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +154 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +105 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +136 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +21 -4
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +507 -0
- package/src/cascade-context.ts +495 -0
- package/src/cascade-executor.ts +588 -0
- package/src/context.ts +51 -17
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/dependency-graph.ts +518 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +412 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +81 -26
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +179 -0
- package/src/types.ts +146 -10
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +199 -355
- package/test/barrier-join.test.ts +442 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +852 -0
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +353 -0
- package/test/send-race-conditions.test.ts +400 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -7
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- 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
|
+
}
|