ai-workflows 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +8 -1
- package/README.md +2 -0
- package/dist/barrier.d.ts +6 -0
- package/dist/barrier.d.ts.map +1 -1
- package/dist/barrier.js +45 -7
- package/dist/barrier.js.map +1 -1
- package/dist/cascade-context.d.ts.map +1 -1
- package/dist/cascade-context.js +25 -25
- package/dist/cascade-context.js.map +1 -1
- package/dist/cascade-executor.d.ts.map +1 -1
- package/dist/cascade-executor.js +1 -1
- package/dist/cascade-executor.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -7
- 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/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/graph/topological-sort.d.ts.map +1 -1
- package/dist/graph/topological-sort.js +5 -5
- package/dist/graph/topological-sort.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -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.map +1 -1
- package/dist/on.js +3 -3
- 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 +25 -0
- package/dist/timer-registry.d.ts.map +1 -1
- package/dist/timer-registry.js +42 -8
- package/dist/timer-registry.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -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 +132 -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 +30 -13
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +48 -7
- package/src/cascade-context.ts +36 -29
- package/src/cascade-executor.ts +3 -2
- package/src/context.ts +41 -12
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -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/graph/topological-sort.ts +6 -8
- package/src/index.ts +69 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +8 -9
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +44 -10
- package/src/types.ts +32 -17
- 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 +188 -351
- package/test/barrier-join.test.ts +32 -24
- package/test/cascade-executor.test.ts +9 -16
- 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/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/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 +30 -21
- package/test/send-race-conditions.test.ts +30 -40
- 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 -169
- package/LICENSE +0 -21
- 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,92 +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,
|
|
30
26
|
EveryProxyTarget,
|
|
31
27
|
ParsedEvent,
|
|
32
|
-
DatabaseContext,
|
|
33
28
|
} from './types.js'
|
|
34
|
-
import {
|
|
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'
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
second: '* * * * * *',
|
|
41
|
-
minute: '* * * * *',
|
|
42
|
-
hour: '0 * * * *',
|
|
43
|
-
day: '0 0 * * *',
|
|
44
|
-
week: '0 0 * * 0',
|
|
45
|
-
month: '0 0 1 * *',
|
|
46
|
-
year: '0 0 1 1 *',
|
|
47
|
-
Monday: '0 0 * * 1',
|
|
48
|
-
Tuesday: '0 0 * * 2',
|
|
49
|
-
Wednesday: '0 0 * * 3',
|
|
50
|
-
Thursday: '0 0 * * 4',
|
|
51
|
-
Friday: '0 0 * * 5',
|
|
52
|
-
Saturday: '0 0 * * 6',
|
|
53
|
-
Sunday: '0 0 * * 0',
|
|
54
|
-
weekday: '0 0 * * 1-5',
|
|
55
|
-
weekend: '0 0 * * 0,6',
|
|
56
|
-
midnight: '0 0 * * *',
|
|
57
|
-
noon: '0 12 * * *',
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Time suffixes for day-based schedules
|
|
62
|
-
*/
|
|
63
|
-
const TIME_PATTERNS: Record<string, { hour: number; minute: number }> = {
|
|
64
|
-
at6am: { hour: 6, minute: 0 },
|
|
65
|
-
at7am: { hour: 7, minute: 0 },
|
|
66
|
-
at8am: { hour: 8, minute: 0 },
|
|
67
|
-
at9am: { hour: 9, minute: 0 },
|
|
68
|
-
at10am: { hour: 10, minute: 0 },
|
|
69
|
-
at11am: { hour: 11, minute: 0 },
|
|
70
|
-
at12pm: { hour: 12, minute: 0 },
|
|
71
|
-
atnoon: { hour: 12, minute: 0 },
|
|
72
|
-
at1pm: { hour: 13, minute: 0 },
|
|
73
|
-
at2pm: { hour: 14, minute: 0 },
|
|
74
|
-
at3pm: { hour: 15, minute: 0 },
|
|
75
|
-
at4pm: { hour: 16, minute: 0 },
|
|
76
|
-
at5pm: { hour: 17, minute: 0 },
|
|
77
|
-
at6pm: { hour: 18, minute: 0 },
|
|
78
|
-
at7pm: { hour: 19, minute: 0 },
|
|
79
|
-
at8pm: { hour: 20, minute: 0 },
|
|
80
|
-
at9pm: { hour: 21, minute: 0 },
|
|
81
|
-
atmidnight: { hour: 0, minute: 0 },
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Combine a day pattern with a time pattern
|
|
86
|
-
*/
|
|
87
|
-
function combineWithTime(baseCron: string, time: { hour: number; minute: number }): string {
|
|
88
|
-
const parts = baseCron.split(' ')
|
|
89
|
-
parts[0] = String(time.minute)
|
|
90
|
-
parts[1] = String(time.hour)
|
|
91
|
-
return parts.join(' ')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 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.
|
|
96
44
|
*/
|
|
97
45
|
export function parseEvent(event: string): ParsedEvent | null {
|
|
98
|
-
|
|
99
|
-
if (parts.length !== 2) {
|
|
100
|
-
return null
|
|
101
|
-
}
|
|
102
|
-
const [noun, eventName] = parts
|
|
103
|
-
if (!noun || !eventName) {
|
|
104
|
-
return null
|
|
105
|
-
}
|
|
106
|
-
return { noun, event: eventName }
|
|
46
|
+
return runtimeParseEvent(event)
|
|
107
47
|
}
|
|
108
48
|
|
|
109
49
|
/**
|
|
@@ -122,6 +62,16 @@ export interface WorkflowInstance {
|
|
|
122
62
|
start: () => Promise<void>
|
|
123
63
|
/** Stop the workflow */
|
|
124
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[]
|
|
125
75
|
}
|
|
126
76
|
|
|
127
77
|
/**
|
|
@@ -152,25 +102,38 @@ export interface WorkflowInstance {
|
|
|
152
102
|
* await workflow.send('Customer.created', { name: 'John', email: 'john@example.com' })
|
|
153
103
|
* ```
|
|
154
104
|
*/
|
|
105
|
+
// Counter for generating unique workflow IDs
|
|
106
|
+
let workflowCounter = 0
|
|
107
|
+
|
|
155
108
|
export function Workflow(
|
|
156
109
|
setup: ($: WorkflowContext) => void,
|
|
157
110
|
options: WorkflowOptions = {}
|
|
158
111
|
): WorkflowInstance {
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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)
|
|
170
131
|
let scheduleTimers: NodeJS.Timeout[] = []
|
|
132
|
+
// Cron jobs for cron/natural schedules
|
|
133
|
+
const cronJobs: CronJob[] = []
|
|
171
134
|
|
|
172
135
|
/**
|
|
173
|
-
*
|
|
136
|
+
* Append to workflow history (used by schedule firing below).
|
|
174
137
|
*/
|
|
175
138
|
const addHistory = (entry: Omit<WorkflowHistoryEntry, 'timestamp'>) => {
|
|
176
139
|
state.history.push({
|
|
@@ -179,251 +142,16 @@ export function Workflow(
|
|
|
179
142
|
})
|
|
180
143
|
}
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
* Register an event handler
|
|
184
|
-
*/
|
|
185
|
-
const registerEventHandler = (noun: string, event: string, handler: EventHandler) => {
|
|
186
|
-
eventRegistry.push({
|
|
187
|
-
noun,
|
|
188
|
-
event,
|
|
189
|
-
handler,
|
|
190
|
-
source: handler.toString(),
|
|
191
|
-
})
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Register a schedule handler
|
|
196
|
-
*/
|
|
197
|
-
const registerScheduleHandler = (interval: ScheduleInterval, handler: ScheduleHandler) => {
|
|
198
|
-
scheduleRegistry.push({
|
|
199
|
-
interval,
|
|
200
|
-
handler,
|
|
201
|
-
source: handler.toString(),
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Create the $.on proxy
|
|
207
|
-
*/
|
|
208
|
-
const createOnProxy = (): OnProxy => {
|
|
209
|
-
return new Proxy({} as OnProxy, {
|
|
210
|
-
get(_target, noun: string) {
|
|
211
|
-
return new Proxy({}, {
|
|
212
|
-
get(_eventTarget, event: string) {
|
|
213
|
-
return (handler: EventHandler) => {
|
|
214
|
-
registerEventHandler(noun, event, handler)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Create the $.every proxy
|
|
224
|
-
*/
|
|
225
|
-
const createEveryProxy = (): EveryProxy => {
|
|
226
|
-
const handler = {
|
|
227
|
-
get(_target: unknown, prop: string) {
|
|
228
|
-
const pattern = KNOWN_PATTERNS[prop]
|
|
229
|
-
if (pattern) {
|
|
230
|
-
const result = (handlerFn: ScheduleHandler) => {
|
|
231
|
-
registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, handlerFn)
|
|
232
|
-
}
|
|
233
|
-
return new Proxy(result, {
|
|
234
|
-
get(_t, timeKey: string) {
|
|
235
|
-
const time = TIME_PATTERNS[timeKey]
|
|
236
|
-
if (time) {
|
|
237
|
-
const cron = combineWithTime(pattern, time)
|
|
238
|
-
return (handlerFn: ScheduleHandler) => {
|
|
239
|
-
registerScheduleHandler({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return undefined
|
|
243
|
-
},
|
|
244
|
-
apply(_t, _thisArg, args) {
|
|
245
|
-
registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, args[0])
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Plural units (seconds, minutes, hours, days, weeks)
|
|
251
|
-
// Using type guard and typed constant for type-safe interval creation
|
|
252
|
-
if (isPluralUnitKey(prop)) {
|
|
253
|
-
const intervalType = PLURAL_UNITS[prop]
|
|
254
|
-
return (value: number) => (handlerFn: ScheduleHandler) => {
|
|
255
|
-
registerScheduleHandler(
|
|
256
|
-
{ type: intervalType, value, natural: `${value} ${prop}` },
|
|
257
|
-
handlerFn
|
|
258
|
-
)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return undefined
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
apply(_target: unknown, _thisArg: unknown, args: unknown[]) {
|
|
266
|
-
const [description, handler] = args as [string, ScheduleHandler]
|
|
267
|
-
if (typeof description === 'string' && typeof handler === 'function') {
|
|
268
|
-
registerScheduleHandler({ type: 'natural', description }, handler)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Create callable target with proper typing
|
|
274
|
-
// The function serves as the Proxy target - actual behavior is in the handler's apply trap
|
|
275
|
-
// Cast to EveryProxy is safe: Proxy handler implements all EveryProxy behaviors dynamically
|
|
276
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
277
|
-
const target: EveryProxyTarget = function(_description: string, _handler: ScheduleHandler) {}
|
|
278
|
-
return new Proxy(target, handler) as unknown as EveryProxy
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Deliver an event to matching handlers (fire and forget)
|
|
283
|
-
*/
|
|
284
|
-
const deliverEvent = async (event: string, data: unknown): Promise<void> => {
|
|
285
|
-
const parsed = parseEvent(event)
|
|
286
|
-
if (!parsed) {
|
|
287
|
-
console.warn(`Invalid event format: ${event}. Expected Noun.event`)
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const matching = eventRegistry.filter(
|
|
292
|
-
h => h.noun === parsed.noun && h.event === parsed.event
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
if (matching.length === 0) {
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
await Promise.all(
|
|
300
|
-
matching.map(async ({ handler }) => {
|
|
301
|
-
try {
|
|
302
|
-
await handler(data, $)
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.error(`Error in handler for ${event}:`, error)
|
|
305
|
-
}
|
|
306
|
-
})
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Execute an event and wait for result from first matching handler
|
|
312
|
-
*/
|
|
313
|
-
const executeEvent = async <TResult = unknown>(
|
|
314
|
-
event: string,
|
|
315
|
-
data: unknown,
|
|
316
|
-
durable: boolean
|
|
317
|
-
): Promise<TResult> => {
|
|
318
|
-
const parsed = parseEvent(event)
|
|
319
|
-
if (!parsed) {
|
|
320
|
-
throw new Error(`Invalid event format: ${event}. Expected Noun.event`)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const matching = eventRegistry.filter(
|
|
324
|
-
h => h.noun === parsed.noun && h.event === parsed.event
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
if (matching.length === 0) {
|
|
328
|
-
throw new Error(`No handler registered for ${event}`)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Use first matching handler for result
|
|
332
|
-
const { handler } = matching[0]!
|
|
333
|
-
|
|
334
|
-
if (durable && options.db) {
|
|
335
|
-
// Create action for durability tracking
|
|
336
|
-
await options.db.createAction({
|
|
337
|
-
actor: 'workflow',
|
|
338
|
-
object: event,
|
|
339
|
-
action: 'execute',
|
|
340
|
-
metadata: { data },
|
|
341
|
-
})
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
const result = await handler(data, $)
|
|
346
|
-
return result as TResult
|
|
347
|
-
} catch (error) {
|
|
348
|
-
if (durable) {
|
|
349
|
-
// Could implement retry logic here
|
|
350
|
-
console.error(`[workflow] Durable action failed for ${event}:`, error)
|
|
351
|
-
}
|
|
352
|
-
throw error
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Create the $ context
|
|
358
|
-
*/
|
|
359
|
-
const $: WorkflowContext = {
|
|
360
|
-
async send<T = unknown>(event: string, data: T): Promise<void> {
|
|
361
|
-
addHistory({ type: 'event', name: event, data })
|
|
362
|
-
|
|
363
|
-
// Record to database if connected (durable)
|
|
364
|
-
if (options.db) {
|
|
365
|
-
await options.db.recordEvent(event, data)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
await deliverEvent(event, data)
|
|
369
|
-
},
|
|
370
|
-
|
|
371
|
-
async do<TData = unknown, TResult = unknown>(event: string, data: TData): Promise<TResult> {
|
|
372
|
-
addHistory({ type: 'action', name: `do:${event}`, data })
|
|
373
|
-
|
|
374
|
-
// Record to database (durable)
|
|
375
|
-
if (options.db) {
|
|
376
|
-
await options.db.recordEvent(event, data)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return executeEvent<TResult>(event, data, true)
|
|
380
|
-
},
|
|
381
|
-
|
|
382
|
-
async try<TData = unknown, TResult = unknown>(event: string, data: TData): Promise<TResult> {
|
|
383
|
-
addHistory({ type: 'action', name: `try:${event}`, data })
|
|
384
|
-
|
|
385
|
-
// Non-durable - no database recording
|
|
386
|
-
return executeEvent<TResult>(event, data, false)
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
on: createOnProxy(),
|
|
390
|
-
every: createEveryProxy(),
|
|
391
|
-
|
|
392
|
-
// Direct access to state context
|
|
393
|
-
state: state.context,
|
|
394
|
-
|
|
395
|
-
getState(): WorkflowState {
|
|
396
|
-
// Return a deep copy to prevent mutation
|
|
397
|
-
return structuredClone({
|
|
398
|
-
current: state.current,
|
|
399
|
-
context: state.context,
|
|
400
|
-
history: state.history,
|
|
401
|
-
})
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
set<T = unknown>(key: string, value: T): void {
|
|
405
|
-
state.context[key] = value
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
get<T = unknown>(key: string): T | undefined {
|
|
409
|
-
return state.context[key] as T | undefined
|
|
410
|
-
},
|
|
411
|
-
|
|
412
|
-
log(message: string, data?: unknown): void {
|
|
413
|
-
addHistory({ type: 'action', name: 'log', data: { message, data } })
|
|
414
|
-
console.log(`[workflow] ${message}`, data ?? '')
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
db: options.db,
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Run setup to capture handlers
|
|
145
|
+
// Run setup to capture handlers via $.on / $.every (which delegate to the runtime).
|
|
421
146
|
setup($)
|
|
422
147
|
|
|
423
148
|
/**
|
|
424
149
|
* Start schedule handlers
|
|
425
150
|
*/
|
|
426
151
|
const startSchedules = async (): Promise<void> => {
|
|
152
|
+
// Register process cleanup on first schedule start
|
|
153
|
+
registerProcessCleanup()
|
|
154
|
+
|
|
427
155
|
for (const schedule of scheduleRegistry) {
|
|
428
156
|
const { interval, handler } = schedule
|
|
429
157
|
|
|
@@ -444,35 +172,104 @@ export function Workflow(
|
|
|
444
172
|
case 'week':
|
|
445
173
|
ms = (interval.value ?? 1) * 7 * 24 * 60 * 60 * 1000
|
|
446
174
|
break
|
|
447
|
-
case 'cron':
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
+
}
|
|
453
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
|
+
}
|
|
454
231
|
}
|
|
455
232
|
|
|
456
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
|
|
457
237
|
const timer = setInterval(async () => {
|
|
458
238
|
try {
|
|
459
|
-
addHistory({ type: 'schedule', name:
|
|
239
|
+
addHistory({ type: 'schedule', name: scheduleName })
|
|
460
240
|
await handler($)
|
|
461
241
|
} catch (error) {
|
|
462
|
-
|
|
242
|
+
getLogger().error('[workflow] Schedule handler error:', error)
|
|
463
243
|
}
|
|
464
244
|
}, ms)
|
|
465
245
|
scheduleTimers.push(timer)
|
|
246
|
+
// Register timer with global registry for cleanup tracking
|
|
247
|
+
registerTimer(workflowId, timer)
|
|
466
248
|
}
|
|
467
249
|
}
|
|
468
250
|
}
|
|
469
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
|
+
|
|
470
267
|
const instance: WorkflowInstance = {
|
|
471
268
|
definition: {
|
|
472
269
|
name: 'workflow',
|
|
473
270
|
events: eventRegistry,
|
|
474
271
|
schedules: scheduleRegistry,
|
|
475
|
-
initialContext: options.context,
|
|
272
|
+
...(options.context !== undefined && { initialContext: options.context }),
|
|
476
273
|
},
|
|
477
274
|
|
|
478
275
|
get state() {
|
|
@@ -486,16 +283,35 @@ export function Workflow(
|
|
|
486
283
|
},
|
|
487
284
|
|
|
488
285
|
async start(): Promise<void> {
|
|
489
|
-
|
|
286
|
+
getLogger().log(
|
|
287
|
+
`[workflow] Starting with ${eventRegistry.length} event handlers and ${scheduleRegistry.length} schedules`
|
|
288
|
+
)
|
|
490
289
|
await startSchedules()
|
|
491
290
|
},
|
|
492
291
|
|
|
493
292
|
async stop(): Promise<void> {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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)
|
|
499
315
|
},
|
|
500
316
|
}
|
|
501
317
|
|
|
@@ -505,7 +321,9 @@ export function Workflow(
|
|
|
505
321
|
/**
|
|
506
322
|
* Create an isolated $ context for testing
|
|
507
323
|
*/
|
|
508
|
-
export function createTestContext(): WorkflowContext & {
|
|
324
|
+
export function createTestContext(): WorkflowContext & {
|
|
325
|
+
emittedEvents: Array<{ event: string; data: unknown }>
|
|
326
|
+
} {
|
|
509
327
|
const emittedEvents: Array<{ event: string; data: unknown }> = []
|
|
510
328
|
const stateContext: Record<string, unknown> = {}
|
|
511
329
|
const history: WorkflowHistoryEntry[] = []
|
|
@@ -513,8 +331,15 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
513
331
|
const $: WorkflowContext & { emittedEvents: Array<{ event: string; data: unknown }> } = {
|
|
514
332
|
emittedEvents,
|
|
515
333
|
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
518
343
|
},
|
|
519
344
|
|
|
520
345
|
async do<TData = unknown, TResult = unknown>(_event: string, _data: TData): Promise<TResult> {
|
|
@@ -527,12 +352,15 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
527
352
|
|
|
528
353
|
on: new Proxy({} as OnProxy, {
|
|
529
354
|
get() {
|
|
530
|
-
return new Proxy(
|
|
531
|
-
|
|
532
|
-
|
|
355
|
+
return new Proxy(
|
|
356
|
+
{},
|
|
357
|
+
{
|
|
358
|
+
get() {
|
|
359
|
+
return () => {} // No-op for testing
|
|
360
|
+
},
|
|
533
361
|
}
|
|
534
|
-
|
|
535
|
-
}
|
|
362
|
+
)
|
|
363
|
+
},
|
|
536
364
|
}),
|
|
537
365
|
|
|
538
366
|
// Cast to EveryProxy is safe: Proxy handler implements all EveryProxy behaviors dynamically
|
|
@@ -543,7 +371,7 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
543
371
|
get() {
|
|
544
372
|
return () => () => {} // No-op for testing
|
|
545
373
|
},
|
|
546
|
-
apply() {}
|
|
374
|
+
apply() {},
|
|
547
375
|
}
|
|
548
376
|
) as unknown as EveryProxy,
|
|
549
377
|
|
|
@@ -565,7 +393,7 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
565
393
|
},
|
|
566
394
|
|
|
567
395
|
log(message: string, data?: unknown) {
|
|
568
|
-
|
|
396
|
+
getLogger().log(`[test] ${message}`, data ?? '')
|
|
569
397
|
},
|
|
570
398
|
}
|
|
571
399
|
|
|
@@ -574,5 +402,14 @@ export function createTestContext(): WorkflowContext & { emittedEvents: Array<{
|
|
|
574
402
|
|
|
575
403
|
// Also export standalone on/every for import { on, every } usage
|
|
576
404
|
export { on, registerEventHandler, getEventHandlers, clearEventHandlers } from './on.js'
|
|
577
|
-
export {
|
|
405
|
+
export {
|
|
406
|
+
every,
|
|
407
|
+
registerScheduleHandler,
|
|
408
|
+
getScheduleHandlers,
|
|
409
|
+
clearScheduleHandlers,
|
|
410
|
+
toCron,
|
|
411
|
+
intervalToMs,
|
|
412
|
+
formatInterval,
|
|
413
|
+
setCronConverter,
|
|
414
|
+
} from './every.js'
|
|
578
415
|
export { send } from './send.js'
|