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/runtime.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowRuntime - Owns the `$` runtime contract
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Single module that owns end-to-end construction of the `$` context
|
|
5
|
+
* a workflow handler receives. Before this module existed the answer to
|
|
6
|
+
* "what does the handler see when it runs?" was stitched together from three
|
|
7
|
+
* separate modules:
|
|
8
|
+
*
|
|
9
|
+
* - cascade-context.ts (5W+H tracing)
|
|
10
|
+
* - database-context.ts (DB injection)
|
|
11
|
+
* - on.ts / send.ts / every.ts (event dispatch)
|
|
12
|
+
*
|
|
13
|
+
* Callers reading `$.on.Order.placed(...)` had no single place to discover
|
|
14
|
+
* what `$` contains. Tests had to assemble three contexts independently to
|
|
15
|
+
* exercise a handler.
|
|
16
|
+
*
|
|
17
|
+
* `WorkflowRuntime` collapses these into one port:
|
|
18
|
+
*
|
|
19
|
+
* - {@link createWorkflowRuntime} builds the runtime, owning the event/
|
|
20
|
+
* schedule registries, the optional `DatabaseContext` injection, and the
|
|
21
|
+
* optional cascade context.
|
|
22
|
+
* - The runtime exposes `$` (a {@link WorkflowContext}) and `dispatch()` —
|
|
23
|
+
* the latter is the test surface for exercising a handler against an
|
|
24
|
+
* event without spinning up a full {@link Workflow}.
|
|
25
|
+
*
|
|
26
|
+
* The cascade-context, database-context, and on/send/every modules continue
|
|
27
|
+
* to exist as **internal seams** — private adapters the runtime composes.
|
|
28
|
+
* `DatabaseContext` stays as an injected port so callers can wire either
|
|
29
|
+
* `ai-database`'s adapter or an in-memory adapter; this keeps `ai-workflows`
|
|
30
|
+
* Layer 0.
|
|
31
|
+
*
|
|
32
|
+
* @example Basic usage
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { createWorkflowRuntime } from 'ai-workflows'
|
|
35
|
+
*
|
|
36
|
+
* const runtime = createWorkflowRuntime()
|
|
37
|
+
* runtime.register('Customer', 'created', async (customer, $) => {
|
|
38
|
+
* $.log('New customer:', customer)
|
|
39
|
+
* })
|
|
40
|
+
* await runtime.dispatch('Customer.created', { id: '123' })
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example With injected DatabaseContext
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { createWorkflowRuntime, createMemoryDatabaseContext } from 'ai-workflows'
|
|
46
|
+
*
|
|
47
|
+
* const runtime = createWorkflowRuntime({ db: createMemoryDatabaseContext() })
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example Direct handler dispatch (test surface)
|
|
51
|
+
* ```ts
|
|
52
|
+
* const runtime = createWorkflowRuntime()
|
|
53
|
+
* const handler = vi.fn()
|
|
54
|
+
* runtime.register('Order', 'placed', handler)
|
|
55
|
+
* await runtime.dispatch('Order.placed', { id: 'o-1' })
|
|
56
|
+
* expect(handler).toHaveBeenCalled()
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @packageDocumentation
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import type {
|
|
63
|
+
DatabaseContext,
|
|
64
|
+
EventHandler,
|
|
65
|
+
EventRegistration,
|
|
66
|
+
ParsedEvent,
|
|
67
|
+
ScheduleHandler,
|
|
68
|
+
ScheduleInterval,
|
|
69
|
+
ScheduleRegistration,
|
|
70
|
+
WorkflowContext,
|
|
71
|
+
WorkflowHistoryEntry,
|
|
72
|
+
WorkflowState,
|
|
73
|
+
DependencyConfig,
|
|
74
|
+
OnProxy,
|
|
75
|
+
EveryProxy,
|
|
76
|
+
} from './types.js'
|
|
77
|
+
import { createTypedOnProxy } from './on.js'
|
|
78
|
+
import { createTypedEveryProxy } from './every.js'
|
|
79
|
+
import { createCascadeContext, type CascadeContext } from './cascade-context.js'
|
|
80
|
+
import { getLogger } from './logger.js'
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse an event string in `Noun.event` form. Returns `null` for invalid
|
|
84
|
+
* input. Lives on the runtime because dispatch is the only thing that needs
|
|
85
|
+
* to crack event names; `workflow.ts` re-exports it for back-compat.
|
|
86
|
+
*/
|
|
87
|
+
export function parseEvent(event: string): ParsedEvent | null {
|
|
88
|
+
const parts = event.split('.')
|
|
89
|
+
if (parts.length !== 2) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
const [noun, eventName] = parts
|
|
93
|
+
if (!noun || !eventName) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
return { noun, event: eventName }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Options for constructing a {@link WorkflowRuntime}.
|
|
101
|
+
*
|
|
102
|
+
* All options are optional — a runtime constructed with no arguments runs
|
|
103
|
+
* fully in-memory with no persistence and no parent trace.
|
|
104
|
+
*/
|
|
105
|
+
export interface WorkflowRuntimeOptions {
|
|
106
|
+
/**
|
|
107
|
+
* Initial state context (key/value bag exposed as `$.state`).
|
|
108
|
+
* Cloned defensively at construction so the caller's object is not mutated.
|
|
109
|
+
*/
|
|
110
|
+
context?: Record<string, unknown>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Optional persistence port. When provided, `$.send` records events and
|
|
114
|
+
* `$.do` records actions through this adapter. When omitted, events and
|
|
115
|
+
* actions are still delivered in-memory but not persisted.
|
|
116
|
+
*/
|
|
117
|
+
db?: DatabaseContext
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Optional cascade context for distributed tracing / 5W+H step recording.
|
|
121
|
+
* If omitted a fresh root cascade context is created.
|
|
122
|
+
*/
|
|
123
|
+
cascade?: CascadeContext
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Optional name for the cascade context (used when cascade is auto-created).
|
|
127
|
+
*/
|
|
128
|
+
name?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Public surface of the workflow runtime.
|
|
133
|
+
*
|
|
134
|
+
* The runtime owns construction of `$` and provides a single dispatch port
|
|
135
|
+
* for delivering events. Internal modules (cascade-context, database-context,
|
|
136
|
+
* on/send/every) are composed here, not exposed.
|
|
137
|
+
*/
|
|
138
|
+
export interface WorkflowRuntime {
|
|
139
|
+
/**
|
|
140
|
+
* The `$` context handed to event and schedule handlers. This is the
|
|
141
|
+
* single source of truth for "what does a handler see when it runs."
|
|
142
|
+
*/
|
|
143
|
+
readonly $: WorkflowContext
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The cascade context owned by this runtime. Exposed for distributed
|
|
147
|
+
* tracing integration; not part of the handler-facing surface.
|
|
148
|
+
*/
|
|
149
|
+
readonly cascade: CascadeContext
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Register an event handler under a noun/event pair.
|
|
153
|
+
*
|
|
154
|
+
* Equivalent to `$.on.<Noun>.<event>(handler)`; provided as a direct method
|
|
155
|
+
* for tests and callers that hold the runtime reference rather than `$`.
|
|
156
|
+
*/
|
|
157
|
+
register(
|
|
158
|
+
noun: string,
|
|
159
|
+
event: string,
|
|
160
|
+
handler: EventHandler,
|
|
161
|
+
dependencies?: DependencyConfig
|
|
162
|
+
): void
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Register a schedule handler under an interval.
|
|
166
|
+
* Schedule timers are not started by the runtime — the {@link Workflow}
|
|
167
|
+
* instance handles that. The runtime only tracks registrations.
|
|
168
|
+
*/
|
|
169
|
+
registerSchedule(interval: ScheduleInterval, handler: ScheduleHandler): void
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Dispatch an event to all matching registered handlers.
|
|
173
|
+
*
|
|
174
|
+
* This is the canonical test surface — exercise a handler by registering
|
|
175
|
+
* it on the runtime and calling `dispatch`. Awaits all handlers; rethrows
|
|
176
|
+
* the first error if any handler throws.
|
|
177
|
+
*/
|
|
178
|
+
dispatch(event: string, data: unknown): Promise<void>
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Dispatch an event and return the result of the first matching handler.
|
|
182
|
+
* Used by `$.do` and `$.try` semantics — `do` is the durable variant that
|
|
183
|
+
* also persists through the database adapter when one is configured.
|
|
184
|
+
*/
|
|
185
|
+
execute<TResult = unknown>(event: string, data: unknown, durable: boolean): Promise<TResult>
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* All event handlers registered on this runtime.
|
|
189
|
+
* Returns the live array (mutated as new handlers register) so callers
|
|
190
|
+
* such as the {@link Workflow} lifecycle wrapper can read it as a definition.
|
|
191
|
+
*/
|
|
192
|
+
getEventRegistry(): EventRegistration[]
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* All schedule handlers registered on this runtime. See {@link getEventRegistry}
|
|
196
|
+
* for sharing semantics.
|
|
197
|
+
*/
|
|
198
|
+
getScheduleRegistry(): ScheduleRegistration[]
|
|
199
|
+
|
|
200
|
+
/** Mutable workflow state (context bag + history). */
|
|
201
|
+
readonly state: WorkflowState
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Internal: append a history entry with the current timestamp.
|
|
206
|
+
*/
|
|
207
|
+
function pushHistory(state: WorkflowState, entry: Omit<WorkflowHistoryEntry, 'timestamp'>): void {
|
|
208
|
+
state.history.push({ ...entry, timestamp: Date.now() })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Construct a {@link WorkflowRuntime}.
|
|
213
|
+
*
|
|
214
|
+
* The runtime is the single owner of the `$` contract — it composes:
|
|
215
|
+
*
|
|
216
|
+
* 1. Event/schedule registries (the dispatch half of on/send/every).
|
|
217
|
+
* 2. The optional injected `DatabaseContext` for durable record-keeping.
|
|
218
|
+
* 3. A cascade context for 5W+H tracing.
|
|
219
|
+
*
|
|
220
|
+
* The returned `runtime.$` is what handlers receive; `runtime.dispatch` is
|
|
221
|
+
* the canonical test surface.
|
|
222
|
+
*/
|
|
223
|
+
export function createWorkflowRuntime(options: WorkflowRuntimeOptions = {}): WorkflowRuntime {
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// State + registries (the runtime is the sole owner)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const state: WorkflowState = {
|
|
229
|
+
context: { ...(options.context ?? {}) },
|
|
230
|
+
history: [],
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const eventRegistry: EventRegistration[] = []
|
|
234
|
+
const scheduleRegistry: ScheduleRegistration[] = []
|
|
235
|
+
|
|
236
|
+
// Cascade context (auto-created if not supplied)
|
|
237
|
+
const cascade =
|
|
238
|
+
options.cascade ??
|
|
239
|
+
createCascadeContext(options.name !== undefined ? { name: options.name } : {})
|
|
240
|
+
|
|
241
|
+
const db = options.db
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Registration (these are the "right halves" of on / every — the registry
|
|
245
|
+
// halves. The proxy halves live in on.ts/every.ts as factories we reuse.)
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
const register: WorkflowRuntime['register'] = (noun, event, handler, dependencies) => {
|
|
249
|
+
eventRegistry.push({
|
|
250
|
+
noun,
|
|
251
|
+
event,
|
|
252
|
+
handler,
|
|
253
|
+
source: handler.toString(),
|
|
254
|
+
...(dependencies !== undefined && { dependencies }),
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const registerSchedule: WorkflowRuntime['registerSchedule'] = (interval, handler) => {
|
|
259
|
+
scheduleRegistry.push({
|
|
260
|
+
interval,
|
|
261
|
+
handler,
|
|
262
|
+
source: handler.toString(),
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Dispatch (the runtime's core: take an event, find handlers, run them)
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
const findMatching = (event: string): EventHandler[] => {
|
|
271
|
+
const parsed = parseEvent(event)
|
|
272
|
+
if (!parsed) {
|
|
273
|
+
getLogger().warn(`Invalid event format: ${event}. Expected Noun.event`)
|
|
274
|
+
return []
|
|
275
|
+
}
|
|
276
|
+
return eventRegistry
|
|
277
|
+
.filter((h) => h.noun === parsed.noun && h.event === parsed.event)
|
|
278
|
+
.map((h) => h.handler)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const dispatch: WorkflowRuntime['dispatch'] = async (event, data) => {
|
|
282
|
+
const matching = findMatching(event)
|
|
283
|
+
if (matching.length === 0) return
|
|
284
|
+
|
|
285
|
+
await Promise.all(
|
|
286
|
+
matching.map(async (handler) => {
|
|
287
|
+
try {
|
|
288
|
+
await handler(data, $)
|
|
289
|
+
} catch (error) {
|
|
290
|
+
getLogger().error(`Error in handler for ${event}:`, error)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const execute: WorkflowRuntime['execute'] = async <TResult = unknown>(
|
|
297
|
+
event: string,
|
|
298
|
+
data: unknown,
|
|
299
|
+
durable: boolean
|
|
300
|
+
): Promise<TResult> => {
|
|
301
|
+
const parsed = parseEvent(event)
|
|
302
|
+
if (!parsed) {
|
|
303
|
+
throw new Error(`Invalid event format: ${event}. Expected Noun.event`)
|
|
304
|
+
}
|
|
305
|
+
const matching = eventRegistry.filter((h) => h.noun === parsed.noun && h.event === parsed.event)
|
|
306
|
+
if (matching.length === 0) {
|
|
307
|
+
throw new Error(`No handler registered for ${event}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { handler } = matching[0]!
|
|
311
|
+
|
|
312
|
+
if (durable && db) {
|
|
313
|
+
await db.createAction({
|
|
314
|
+
actor: 'workflow',
|
|
315
|
+
object: event,
|
|
316
|
+
action: 'execute',
|
|
317
|
+
metadata: { data },
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const result = await handler(data, $)
|
|
323
|
+
return result as TResult
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (durable) {
|
|
326
|
+
getLogger().error(`[runtime] Durable action failed for ${event}:`, error)
|
|
327
|
+
}
|
|
328
|
+
throw error
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Build the `$` context. This is the WHOLE answer to "what does a handler
|
|
334
|
+
// see when it runs?" — every property below is a deliberate part of the
|
|
335
|
+
// handler-facing surface.
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
const onProxy: OnProxy = createTypedOnProxy((noun, event, handler, deps) => {
|
|
339
|
+
register(noun, event, handler, deps)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const everyProxy: EveryProxy = createTypedEveryProxy((interval, handler) => {
|
|
343
|
+
registerSchedule(interval, handler)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const $: WorkflowContext = {
|
|
347
|
+
track(event: string, data: unknown): void {
|
|
348
|
+
try {
|
|
349
|
+
pushHistory(state, { type: 'event', name: `track:${event}`, data })
|
|
350
|
+
dispatch(event, data).catch(() => {
|
|
351
|
+
// track() swallows errors by design
|
|
352
|
+
})
|
|
353
|
+
} catch {
|
|
354
|
+
// Silently swallow errors
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
send<T = unknown>(event: string, data: T): string {
|
|
359
|
+
const eventId = crypto.randomUUID()
|
|
360
|
+
pushHistory(state, { type: 'event', name: event, data })
|
|
361
|
+
|
|
362
|
+
const payload = { ...(data as object), _eventId: eventId }
|
|
363
|
+
|
|
364
|
+
// Persist if a DatabaseContext is wired
|
|
365
|
+
if (db) {
|
|
366
|
+
db.recordEvent(event, payload).catch((err) => {
|
|
367
|
+
getLogger().error(`[runtime] Failed to record event ${event}:`, err)
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Deliver
|
|
372
|
+
dispatch(event, payload).catch((err) => {
|
|
373
|
+
getLogger().error(`[runtime] Failed to deliver event ${event}:`, err)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
return eventId
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
async do<TResult = unknown, TInput = unknown>(event: string, data: TInput): Promise<TResult> {
|
|
380
|
+
pushHistory(state, { type: 'action', name: `do:${event}`, data })
|
|
381
|
+
if (db) {
|
|
382
|
+
await db.recordEvent(event, data)
|
|
383
|
+
}
|
|
384
|
+
return execute<TResult>(event, data, true)
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
async try<TResult = unknown, TInput = unknown>(event: string, data: TInput): Promise<TResult> {
|
|
388
|
+
pushHistory(state, { type: 'action', name: `try:${event}`, data })
|
|
389
|
+
return execute<TResult>(event, data, false)
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
on: onProxy,
|
|
393
|
+
every: everyProxy,
|
|
394
|
+
|
|
395
|
+
state: state.context,
|
|
396
|
+
|
|
397
|
+
getState(): WorkflowState {
|
|
398
|
+
return structuredClone({
|
|
399
|
+
...(state.current !== undefined && { current: state.current }),
|
|
400
|
+
context: state.context,
|
|
401
|
+
history: state.history,
|
|
402
|
+
})
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
set<T = unknown>(key: string, value: T): void {
|
|
406
|
+
state.context[key] = value
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
get<T = unknown>(key: string): T | undefined {
|
|
410
|
+
return state.context[key] as T | undefined
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
log(message: string, data?: unknown): void {
|
|
414
|
+
pushHistory(state, { type: 'action', name: 'log', data: { message, data } })
|
|
415
|
+
getLogger().log(`[workflow] ${message}`, data ?? '')
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
...(db !== undefined && { db }),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// Public surface
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
$,
|
|
427
|
+
cascade,
|
|
428
|
+
register,
|
|
429
|
+
registerSchedule,
|
|
430
|
+
dispatch,
|
|
431
|
+
execute,
|
|
432
|
+
getEventRegistry: () => eventRegistry,
|
|
433
|
+
getScheduleRegistry: () => scheduleRegistry,
|
|
434
|
+
state,
|
|
435
|
+
}
|
|
436
|
+
}
|
package/src/send.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { getEventHandlers } from './on.js'
|
|
10
10
|
import { createWorkflowContext } from './context.js'
|
|
11
11
|
import { parseEvent } from './workflow.js'
|
|
12
|
+
import { getLogger } from './logger.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Event bus for managing event delivery
|
|
@@ -48,14 +49,12 @@ class EventBus {
|
|
|
48
49
|
private async deliver(event: string, data: unknown): Promise<void> {
|
|
49
50
|
const parsed = parseEvent(event)
|
|
50
51
|
if (!parsed) {
|
|
51
|
-
|
|
52
|
+
getLogger().warn(`Invalid event format: ${event}. Expected Noun.event`)
|
|
52
53
|
return
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
const handlers = getEventHandlers()
|
|
56
|
-
const matching = handlers.filter(
|
|
57
|
-
h => h.noun === parsed.noun && h.event === parsed.event
|
|
58
|
-
)
|
|
57
|
+
const matching = handlers.filter((h) => h.noun === parsed.noun && h.event === parsed.event)
|
|
59
58
|
|
|
60
59
|
if (matching.length === 0) {
|
|
61
60
|
// No handlers registered - that's okay, event is just not handled
|
|
@@ -71,7 +70,7 @@ class EventBus {
|
|
|
71
70
|
try {
|
|
72
71
|
await handler(data, ctx)
|
|
73
72
|
} catch (error) {
|
|
74
|
-
|
|
73
|
+
getLogger().error(`Error in handler for ${event}:`, error)
|
|
75
74
|
}
|
|
76
75
|
})
|
|
77
76
|
)
|