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
package/src/workflow.ts
CHANGED
|
@@ -18,90 +18,32 @@ import type {
|
|
|
18
18
|
WorkflowContext,
|
|
19
19
|
WorkflowState,
|
|
20
20
|
WorkflowHistoryEntry,
|
|
21
|
-
EventHandler,
|
|
22
21
|
ScheduleHandler,
|
|
23
|
-
EventRegistration,
|
|
24
|
-
ScheduleRegistration,
|
|
25
|
-
ScheduleInterval,
|
|
26
22
|
WorkflowDefinition,
|
|
27
23
|
WorkflowOptions,
|
|
28
24
|
OnProxy,
|
|
29
25
|
EveryProxy,
|
|
26
|
+
EveryProxyTarget,
|
|
30
27
|
ParsedEvent,
|
|
31
|
-
DatabaseContext,
|
|
32
28
|
} from './types.js'
|
|
29
|
+
import {
|
|
30
|
+
registerTimer,
|
|
31
|
+
clearTimersForWorkflow,
|
|
32
|
+
getTimerIdsForWorkflow,
|
|
33
|
+
registerProcessCleanup,
|
|
34
|
+
} from './timer-registry.js'
|
|
35
|
+
import { createCronJob, stopCronJob, type CronJob } from './cron-scheduler.js'
|
|
36
|
+
import { toCron } from './every.js'
|
|
37
|
+
import { getLogger } from './logger.js'
|
|
38
|
+
import { createWorkflowRuntime, parseEvent as runtimeParseEvent } from './runtime.js'
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
second: '* * * * * *',
|
|
39
|
-
minute: '* * * * *',
|
|
40
|
-
hour: '0 * * * *',
|
|
41
|
-
day: '0 0 * * *',
|
|
42
|
-
week: '0 0 * * 0',
|
|
43
|
-
month: '0 0 1 * *',
|
|
44
|
-
year: '0 0 1 1 *',
|
|
45
|
-
Monday: '0 0 * * 1',
|
|
46
|
-
Tuesday: '0 0 * * 2',
|
|
47
|
-
Wednesday: '0 0 * * 3',
|
|
48
|
-
Thursday: '0 0 * * 4',
|
|
49
|
-
Friday: '0 0 * * 5',
|
|
50
|
-
Saturday: '0 0 * * 6',
|
|
51
|
-
Sunday: '0 0 * * 0',
|
|
52
|
-
weekday: '0 0 * * 1-5',
|
|
53
|
-
weekend: '0 0 * * 0,6',
|
|
54
|
-
midnight: '0 0 * * *',
|
|
55
|
-
noon: '0 12 * * *',
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Time suffixes for day-based schedules
|
|
60
|
-
*/
|
|
61
|
-
const TIME_PATTERNS: Record<string, { hour: number; minute: number }> = {
|
|
62
|
-
at6am: { hour: 6, minute: 0 },
|
|
63
|
-
at7am: { hour: 7, minute: 0 },
|
|
64
|
-
at8am: { hour: 8, minute: 0 },
|
|
65
|
-
at9am: { hour: 9, minute: 0 },
|
|
66
|
-
at10am: { hour: 10, minute: 0 },
|
|
67
|
-
at11am: { hour: 11, minute: 0 },
|
|
68
|
-
at12pm: { hour: 12, minute: 0 },
|
|
69
|
-
atnoon: { hour: 12, minute: 0 },
|
|
70
|
-
at1pm: { hour: 13, minute: 0 },
|
|
71
|
-
at2pm: { hour: 14, minute: 0 },
|
|
72
|
-
at3pm: { hour: 15, minute: 0 },
|
|
73
|
-
at4pm: { hour: 16, minute: 0 },
|
|
74
|
-
at5pm: { hour: 17, minute: 0 },
|
|
75
|
-
at6pm: { hour: 18, minute: 0 },
|
|
76
|
-
at7pm: { hour: 19, minute: 0 },
|
|
77
|
-
at8pm: { hour: 20, minute: 0 },
|
|
78
|
-
at9pm: { hour: 21, minute: 0 },
|
|
79
|
-
atmidnight: { hour: 0, minute: 0 },
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Combine a day pattern with a time pattern
|
|
84
|
-
*/
|
|
85
|
-
function combineWithTime(baseCron: string, time: { hour: number; minute: number }): string {
|
|
86
|
-
const parts = baseCron.split(' ')
|
|
87
|
-
parts[0] = String(time.minute)
|
|
88
|
-
parts[1] = String(time.hour)
|
|
89
|
-
return parts.join(' ')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Parse event string into noun and event
|
|
41
|
+
* Parse event string into noun and event.
|
|
42
|
+
* Re-exported from runtime.ts for backward compatibility — the canonical
|
|
43
|
+
* implementation lives there because dispatch owns event-name parsing.
|
|
94
44
|
*/
|
|
95
45
|
export function parseEvent(event: string): ParsedEvent | null {
|
|
96
|
-
|
|
97
|
-
if (parts.length !== 2) {
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
const [noun, eventName] = parts
|
|
101
|
-
if (!noun || !eventName) {
|
|
102
|
-
return null
|
|
103
|
-
}
|
|
104
|
-
return { noun, event: eventName }
|
|
46
|
+
return runtimeParseEvent(event)
|
|
105
47
|
}
|
|
106
48
|
|
|
107
49
|
/**
|
|
@@ -120,6 +62,16 @@ export interface WorkflowInstance {
|
|
|
120
62
|
start: () => Promise<void>
|
|
121
63
|
/** Stop the workflow */
|
|
122
64
|
stop: () => Promise<void>
|
|
65
|
+
/** Destroy the workflow and clean up all resources */
|
|
66
|
+
destroy: () => Promise<void>
|
|
67
|
+
/** Dispose pattern for cleanup */
|
|
68
|
+
dispose: () => void
|
|
69
|
+
/** Symbol.dispose for using declaration support */
|
|
70
|
+
[Symbol.dispose]: () => void
|
|
71
|
+
/** Number of active timers */
|
|
72
|
+
timerCount: number
|
|
73
|
+
/** Get timer IDs for this workflow */
|
|
74
|
+
getTimerIds: () => string[]
|
|
123
75
|
}
|
|
124
76
|
|
|
125
77
|
/**
|
|
@@ -150,25 +102,38 @@ export interface WorkflowInstance {
|
|
|
150
102
|
* await workflow.send('Customer.created', { name: 'John', email: 'john@example.com' })
|
|
151
103
|
* ```
|
|
152
104
|
*/
|
|
105
|
+
// Counter for generating unique workflow IDs
|
|
106
|
+
let workflowCounter = 0
|
|
107
|
+
|
|
153
108
|
export function Workflow(
|
|
154
109
|
setup: ($: WorkflowContext) => void,
|
|
155
110
|
options: WorkflowOptions = {}
|
|
156
111
|
): WorkflowInstance {
|
|
157
|
-
//
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
112
|
+
// Generate unique workflow ID
|
|
113
|
+
const workflowId = `workflow-${++workflowCounter}-${Date.now()}`
|
|
114
|
+
|
|
115
|
+
// Construct the runtime — it owns the $ contract end-to-end:
|
|
116
|
+
// event registry, schedule registry, dispatch, history, and state. The
|
|
117
|
+
// Workflow wrapper here is just a lifecycle shell around the runtime
|
|
118
|
+
// (timers, cron jobs, dispose pattern).
|
|
119
|
+
const runtime = createWorkflowRuntime({
|
|
120
|
+
...(options.context !== undefined && { context: options.context }),
|
|
121
|
+
...(options.db !== undefined && { db: options.db }),
|
|
122
|
+
name: 'workflow',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const $ = runtime.$
|
|
126
|
+
const state = runtime.state
|
|
127
|
+
const eventRegistry = runtime.getEventRegistry()
|
|
128
|
+
const scheduleRegistry = runtime.getScheduleRegistry()
|
|
129
|
+
|
|
130
|
+
// Schedule timers (local reference, actual timers are in registry)
|
|
168
131
|
let scheduleTimers: NodeJS.Timeout[] = []
|
|
132
|
+
// Cron jobs for cron/natural schedules
|
|
133
|
+
const cronJobs: CronJob[] = []
|
|
169
134
|
|
|
170
135
|
/**
|
|
171
|
-
*
|
|
136
|
+
* Append to workflow history (used by schedule firing below).
|
|
172
137
|
*/
|
|
173
138
|
const addHistory = (entry: Omit<WorkflowHistoryEntry, 'timestamp'>) => {
|
|
174
139
|
state.history.push({
|
|
@@ -177,251 +142,16 @@ export function Workflow(
|
|
|
177
142
|
})
|
|
178
143
|
}
|
|
179
144
|
|
|
180
|
-
|
|
181
|
-
* Register an event handler
|
|
182
|
-
*/
|
|
183
|
-
const registerEventHandler = (noun: string, event: string, handler: EventHandler) => {
|
|
184
|
-
eventRegistry.push({
|
|
185
|
-
noun,
|
|
186
|
-
event,
|
|
187
|
-
handler,
|
|
188
|
-
source: handler.toString(),
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Register a schedule handler
|
|
194
|
-
*/
|
|
195
|
-
const registerScheduleHandler = (interval: ScheduleInterval, handler: ScheduleHandler) => {
|
|
196
|
-
scheduleRegistry.push({
|
|
197
|
-
interval,
|
|
198
|
-
handler,
|
|
199
|
-
source: handler.toString(),
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Create the $.on proxy
|
|
205
|
-
*/
|
|
206
|
-
const createOnProxy = (): OnProxy => {
|
|
207
|
-
return new Proxy({} as OnProxy, {
|
|
208
|
-
get(_target, noun: string) {
|
|
209
|
-
return new Proxy({}, {
|
|
210
|
-
get(_eventTarget, event: string) {
|
|
211
|
-
return (handler: EventHandler) => {
|
|
212
|
-
registerEventHandler(noun, event, handler)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Create the $.every proxy
|
|
222
|
-
*/
|
|
223
|
-
const createEveryProxy = (): EveryProxy => {
|
|
224
|
-
const handler = {
|
|
225
|
-
get(_target: unknown, prop: string) {
|
|
226
|
-
const pattern = KNOWN_PATTERNS[prop]
|
|
227
|
-
if (pattern) {
|
|
228
|
-
const result = (handlerFn: ScheduleHandler) => {
|
|
229
|
-
registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, handlerFn)
|
|
230
|
-
}
|
|
231
|
-
return new Proxy(result, {
|
|
232
|
-
get(_t, timeKey: string) {
|
|
233
|
-
const time = TIME_PATTERNS[timeKey]
|
|
234
|
-
if (time) {
|
|
235
|
-
const cron = combineWithTime(pattern, time)
|
|
236
|
-
return (handlerFn: ScheduleHandler) => {
|
|
237
|
-
registerScheduleHandler({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return undefined
|
|
241
|
-
},
|
|
242
|
-
apply(_t, _thisArg, args) {
|
|
243
|
-
registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, args[0])
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Plural units (seconds, minutes, hours, days, weeks)
|
|
249
|
-
const pluralUnits: Record<string, string> = {
|
|
250
|
-
seconds: 'second',
|
|
251
|
-
minutes: 'minute',
|
|
252
|
-
hours: 'hour',
|
|
253
|
-
days: 'day',
|
|
254
|
-
weeks: 'week',
|
|
255
|
-
}
|
|
256
|
-
if (pluralUnits[prop]) {
|
|
257
|
-
return (value: number) => (handlerFn: ScheduleHandler) => {
|
|
258
|
-
registerScheduleHandler(
|
|
259
|
-
{ type: pluralUnits[prop] as any, value, natural: `${value} ${prop}` },
|
|
260
|
-
handlerFn
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return undefined
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
apply(_target: unknown, _thisArg: unknown, args: unknown[]) {
|
|
269
|
-
const [description, handler] = args as [string, ScheduleHandler]
|
|
270
|
-
if (typeof description === 'string' && typeof handler === 'function') {
|
|
271
|
-
registerScheduleHandler({ type: 'natural', description }, handler)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return new Proxy(function() {} as any, handler)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Deliver an event to matching handlers (fire and forget)
|
|
281
|
-
*/
|
|
282
|
-
const deliverEvent = async (event: string, data: unknown): Promise<void> => {
|
|
283
|
-
const parsed = parseEvent(event)
|
|
284
|
-
if (!parsed) {
|
|
285
|
-
console.warn(`Invalid event format: ${event}. Expected Noun.event`)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const matching = eventRegistry.filter(
|
|
290
|
-
h => h.noun === parsed.noun && h.event === parsed.event
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
if (matching.length === 0) {
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
await Promise.all(
|
|
298
|
-
matching.map(async ({ handler }) => {
|
|
299
|
-
try {
|
|
300
|
-
await handler(data, $)
|
|
301
|
-
} catch (error) {
|
|
302
|
-
console.error(`Error in handler for ${event}:`, error)
|
|
303
|
-
}
|
|
304
|
-
})
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Execute an event and wait for result from first matching handler
|
|
310
|
-
*/
|
|
311
|
-
const executeEvent = async <TResult = unknown>(
|
|
312
|
-
event: string,
|
|
313
|
-
data: unknown,
|
|
314
|
-
durable: boolean
|
|
315
|
-
): Promise<TResult> => {
|
|
316
|
-
const parsed = parseEvent(event)
|
|
317
|
-
if (!parsed) {
|
|
318
|
-
throw new Error(`Invalid event format: ${event}. Expected Noun.event`)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const matching = eventRegistry.filter(
|
|
322
|
-
h => h.noun === parsed.noun && h.event === parsed.event
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
if (matching.length === 0) {
|
|
326
|
-
throw new Error(`No handler registered for ${event}`)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Use first matching handler for result
|
|
330
|
-
const { handler } = matching[0]!
|
|
331
|
-
|
|
332
|
-
if (durable && options.db) {
|
|
333
|
-
// Create action for durability tracking
|
|
334
|
-
await options.db.createAction({
|
|
335
|
-
actor: 'workflow',
|
|
336
|
-
object: event,
|
|
337
|
-
action: 'execute',
|
|
338
|
-
metadata: { data },
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const result = await handler(data, $)
|
|
344
|
-
return result as TResult
|
|
345
|
-
} catch (error) {
|
|
346
|
-
if (durable) {
|
|
347
|
-
// Could implement retry logic here
|
|
348
|
-
console.error(`[workflow] Durable action failed for ${event}:`, error)
|
|
349
|
-
}
|
|
350
|
-
throw error
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Create the $ context
|
|
356
|
-
*/
|
|
357
|
-
const $: WorkflowContext = {
|
|
358
|
-
async send<T = unknown>(event: string, data: T): Promise<void> {
|
|
359
|
-
addHistory({ type: 'event', name: event, data })
|
|
360
|
-
|
|
361
|
-
// Record to database if connected (durable)
|
|
362
|
-
if (options.db) {
|
|
363
|
-
await options.db.recordEvent(event, data)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
await deliverEvent(event, data)
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
async do<TData = unknown, TResult = unknown>(event: string, data: TData): Promise<TResult> {
|
|
370
|
-
addHistory({ type: 'action', name: `do:${event}`, data })
|
|
371
|
-
|
|
372
|
-
// Record to database (durable)
|
|
373
|
-
if (options.db) {
|
|
374
|
-
await options.db.recordEvent(event, data)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return executeEvent<TResult>(event, data, true)
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
async try<TData = unknown, TResult = unknown>(event: string, data: TData): Promise<TResult> {
|
|
381
|
-
addHistory({ type: 'action', name: `try:${event}`, data })
|
|
382
|
-
|
|
383
|
-
// Non-durable - no database recording
|
|
384
|
-
return executeEvent<TResult>(event, data, false)
|
|
385
|
-
},
|
|
386
|
-
|
|
387
|
-
on: createOnProxy(),
|
|
388
|
-
every: createEveryProxy(),
|
|
389
|
-
|
|
390
|
-
// Direct access to state context
|
|
391
|
-
state: state.context,
|
|
392
|
-
|
|
393
|
-
getState(): WorkflowState {
|
|
394
|
-
// Return a deep copy to prevent mutation
|
|
395
|
-
return structuredClone({
|
|
396
|
-
current: state.current,
|
|
397
|
-
context: state.context,
|
|
398
|
-
history: state.history,
|
|
399
|
-
})
|
|
400
|
-
},
|
|
401
|
-
|
|
402
|
-
set<T = unknown>(key: string, value: T): void {
|
|
403
|
-
state.context[key] = value
|
|
404
|
-
},
|
|
405
|
-
|
|
406
|
-
get<T = unknown>(key: string): T | undefined {
|
|
407
|
-
return state.context[key] as T | undefined
|
|
408
|
-
},
|
|
409
|
-
|
|
410
|
-
log(message: string, data?: unknown): void {
|
|
411
|
-
addHistory({ type: 'action', name: 'log', data: { message, data } })
|
|
412
|
-
console.log(`[workflow] ${message}`, data ?? '')
|
|
413
|
-
},
|
|
414
|
-
|
|
415
|
-
db: options.db,
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Run setup to capture handlers
|
|
145
|
+
// Run setup to capture handlers via $.on / $.every (which delegate to the runtime).
|
|
419
146
|
setup($)
|
|
420
147
|
|
|
421
148
|
/**
|
|
422
149
|
* Start schedule handlers
|
|
423
150
|
*/
|
|
424
151
|
const startSchedules = async (): Promise<void> => {
|
|
152
|
+
// Register process cleanup on first schedule start
|
|
153
|
+
registerProcessCleanup()
|
|
154
|
+
|
|
425
155
|
for (const schedule of scheduleRegistry) {
|
|
426
156
|
const { interval, handler } = schedule
|
|
427
157
|
|
|
@@ -442,35 +172,104 @@ export function Workflow(
|
|
|
442
172
|
case 'week':
|
|
443
173
|
ms = (interval.value ?? 1) * 7 * 24 * 60 * 60 * 1000
|
|
444
174
|
break
|
|
445
|
-
case 'cron':
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
175
|
+
case 'cron': {
|
|
176
|
+
// Schedule using cron expression
|
|
177
|
+
const cronExpression = interval.expression
|
|
178
|
+
const job = createCronJob(
|
|
179
|
+
cronExpression,
|
|
180
|
+
async () => {
|
|
181
|
+
try {
|
|
182
|
+
addHistory({ type: 'schedule', name: interval.natural ?? `cron:${cronExpression}` })
|
|
183
|
+
await handler($)
|
|
184
|
+
} catch (error) {
|
|
185
|
+
getLogger().error('[workflow] Cron schedule handler error:', error)
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: `${workflowId}-cron-${scheduleRegistry.indexOf(schedule)}`,
|
|
190
|
+
onError: (error) => {
|
|
191
|
+
getLogger().error('[workflow] Cron job error:', error)
|
|
192
|
+
},
|
|
193
|
+
}
|
|
451
194
|
)
|
|
195
|
+
cronJobs.push(job)
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
case 'natural': {
|
|
199
|
+
// Convert natural language to cron using toCron()
|
|
200
|
+
// This may be async if AI converter is set
|
|
201
|
+
const naturalDesc = interval.description
|
|
202
|
+
toCron(naturalDesc)
|
|
203
|
+
.then((cronExpression) => {
|
|
204
|
+
const job = createCronJob(
|
|
205
|
+
cronExpression,
|
|
206
|
+
async () => {
|
|
207
|
+
try {
|
|
208
|
+
addHistory({ type: 'schedule', name: naturalDesc })
|
|
209
|
+
await handler($)
|
|
210
|
+
} catch (error) {
|
|
211
|
+
getLogger().error('[workflow] Natural schedule handler error:', error)
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: `${workflowId}-natural-${scheduleRegistry.indexOf(schedule)}`,
|
|
216
|
+
onError: (error) => {
|
|
217
|
+
getLogger().error('[workflow] Natural schedule job error:', error)
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
cronJobs.push(job)
|
|
222
|
+
})
|
|
223
|
+
.catch((error) => {
|
|
224
|
+
getLogger().error(
|
|
225
|
+
`[workflow] Failed to parse natural schedule "${naturalDesc}":`,
|
|
226
|
+
error
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
break
|
|
230
|
+
}
|
|
452
231
|
}
|
|
453
232
|
|
|
454
233
|
if (ms > 0) {
|
|
234
|
+
// Get schedule name based on interval type
|
|
235
|
+
const scheduleName =
|
|
236
|
+
'natural' in interval && interval.natural ? interval.natural : interval.type
|
|
455
237
|
const timer = setInterval(async () => {
|
|
456
238
|
try {
|
|
457
|
-
addHistory({ type: 'schedule', name:
|
|
239
|
+
addHistory({ type: 'schedule', name: scheduleName })
|
|
458
240
|
await handler($)
|
|
459
241
|
} catch (error) {
|
|
460
|
-
|
|
242
|
+
getLogger().error('[workflow] Schedule handler error:', error)
|
|
461
243
|
}
|
|
462
244
|
}, ms)
|
|
463
245
|
scheduleTimers.push(timer)
|
|
246
|
+
// Register timer with global registry for cleanup tracking
|
|
247
|
+
registerTimer(workflowId, timer)
|
|
464
248
|
}
|
|
465
249
|
}
|
|
466
250
|
}
|
|
467
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Clean up all timers and resources
|
|
254
|
+
*/
|
|
255
|
+
const cleanup = (): void => {
|
|
256
|
+
// Clear via global registry (this also clears the intervals)
|
|
257
|
+
clearTimersForWorkflow(workflowId)
|
|
258
|
+
// Clear local references
|
|
259
|
+
scheduleTimers = []
|
|
260
|
+
// Stop all cron jobs
|
|
261
|
+
for (const job of cronJobs) {
|
|
262
|
+
stopCronJob(job)
|
|
263
|
+
}
|
|
264
|
+
cronJobs.length = 0
|
|
265
|
+
}
|
|
266
|
+
|
|
468
267
|
const instance: WorkflowInstance = {
|
|
469
268
|
definition: {
|
|
470
269
|
name: 'workflow',
|
|
471
270
|
events: eventRegistry,
|
|
472
271
|
schedules: scheduleRegistry,
|
|
473
|
-
initialContext: options.context,
|
|
272
|
+
...(options.context !== undefined && { initialContext: options.context }),
|
|
474
273
|
},
|
|
475
274
|
|
|
476
275
|
get state() {
|
|
@@ -484,16 +283,35 @@ export function Workflow(
|
|
|
484
283
|
},
|
|
485
284
|
|
|
486
285
|
async start(): Promise<void> {
|
|
487
|
-
|
|
286
|
+
getLogger().log(
|
|
287
|
+
`[workflow] Starting with ${eventRegistry.length} event handlers and ${scheduleRegistry.length} schedules`
|
|
288
|
+
)
|
|
488
289
|
await startSchedules()
|
|
489
290
|
},
|
|
490
291
|
|
|
491
292
|
async stop(): Promise<void> {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
293
|
+
getLogger().log('[workflow] Stopping')
|
|
294
|
+
cleanup()
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async destroy(): Promise<void> {
|
|
298
|
+
cleanup()
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
dispose(): void {
|
|
302
|
+
cleanup()
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
[Symbol.dispose](): void {
|
|
306
|
+
cleanup()
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
get timerCount(): number {
|
|
310
|
+
return getTimerIdsForWorkflow(workflowId).length + cronJobs.filter((j) => !j.stopped).length
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
getTimerIds(): string[] {
|
|
314
|
+
return getTimerIdsForWorkflow(workflowId)
|
|
497
315
|
},
|
|
498
316
|
}
|
|
499
317
|
|
|
@@ -503,7 +321,9 @@ export function Workflow(
|
|
|
503
321
|
/**
|
|
504
322
|
* Create an isolated $ context for testing
|
|
505
323
|
*/
|
|
506
|
-
export function createTestContext(): WorkflowContext & {
|
|
324
|
+
export function createTestContext(): WorkflowContext & {
|
|
325
|
+
emittedEvents: Array<{ event: string; data: unknown }>
|
|
326
|
+
} {
|
|
507
327
|
const emittedEvents: Array<{ event: string; data: unknown }> = []
|
|
508
328
|
const stateContext: Record<string, unknown> = {}
|
|
509
329
|
const history: WorkflowHistoryEntry[] = []
|
|
@@ -511,8 +331,15 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
511
331
|
const $: WorkflowContext & { emittedEvents: Array<{ event: string; data: unknown }> } = {
|
|
512
332
|
emittedEvents,
|
|
513
333
|
|
|
514
|
-
|
|
515
|
-
|
|
334
|
+
track(event: string, data: unknown): void {
|
|
335
|
+
// Fire and forget for testing - just record it
|
|
336
|
+
emittedEvents.push({ event: `track:${event}`, data })
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
send<T = unknown>(event: string, data: T): string {
|
|
340
|
+
const eventId = crypto.randomUUID()
|
|
341
|
+
emittedEvents.push({ event, data: { ...(data as object), _eventId: eventId } })
|
|
342
|
+
return eventId
|
|
516
343
|
},
|
|
517
344
|
|
|
518
345
|
async do<TData = unknown, TResult = unknown>(_event: string, _data: TData): Promise<TResult> {
|
|
@@ -525,21 +352,29 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
525
352
|
|
|
526
353
|
on: new Proxy({} as OnProxy, {
|
|
527
354
|
get() {
|
|
528
|
-
return new Proxy(
|
|
529
|
-
|
|
530
|
-
|
|
355
|
+
return new Proxy(
|
|
356
|
+
{},
|
|
357
|
+
{
|
|
358
|
+
get() {
|
|
359
|
+
return () => {} // No-op for testing
|
|
360
|
+
},
|
|
531
361
|
}
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
}),
|
|
535
|
-
|
|
536
|
-
every: new Proxy(function() {} as any, {
|
|
537
|
-
get() {
|
|
538
|
-
return () => () => {} // No-op for testing
|
|
362
|
+
)
|
|
539
363
|
},
|
|
540
|
-
apply() {}
|
|
541
364
|
}),
|
|
542
365
|
|
|
366
|
+
// Cast to EveryProxy is safe: Proxy handler implements all EveryProxy behaviors dynamically
|
|
367
|
+
every: new Proxy(
|
|
368
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
369
|
+
((_description: string, _handler: ScheduleHandler) => {}) as EveryProxyTarget,
|
|
370
|
+
{
|
|
371
|
+
get() {
|
|
372
|
+
return () => () => {} // No-op for testing
|
|
373
|
+
},
|
|
374
|
+
apply() {},
|
|
375
|
+
}
|
|
376
|
+
) as unknown as EveryProxy,
|
|
377
|
+
|
|
543
378
|
state: stateContext,
|
|
544
379
|
|
|
545
380
|
getState(): WorkflowState {
|
|
@@ -558,7 +393,7 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
558
393
|
},
|
|
559
394
|
|
|
560
395
|
log(message: string, data?: unknown) {
|
|
561
|
-
|
|
396
|
+
getLogger().log(`[test] ${message}`, data ?? '')
|
|
562
397
|
},
|
|
563
398
|
}
|
|
564
399
|
|
|
@@ -567,5 +402,14 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
567
402
|
|
|
568
403
|
// Also export standalone on/every for import { on, every } usage
|
|
569
404
|
export { on, registerEventHandler, getEventHandlers, clearEventHandlers } from './on.js'
|
|
570
|
-
export {
|
|
405
|
+
export {
|
|
406
|
+
every,
|
|
407
|
+
registerScheduleHandler,
|
|
408
|
+
getScheduleHandlers,
|
|
409
|
+
clearScheduleHandlers,
|
|
410
|
+
toCron,
|
|
411
|
+
intervalToMs,
|
|
412
|
+
formatInterval,
|
|
413
|
+
setCronConverter,
|
|
414
|
+
} from './every.js'
|
|
571
415
|
export { send } from './send.js'
|