ai-workflows 2.1.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +14 -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/dist/worker.js
ADDED
|
@@ -0,0 +1,2428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Export - WorkerEntrypoint for RPC access to AI Workflows
|
|
3
|
+
*
|
|
4
|
+
* Exposes workflow functionality via Cloudflare RPC.
|
|
5
|
+
* Works both in Cloudflare Workers and standalone (for testing).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // wrangler.jsonc
|
|
10
|
+
* {
|
|
11
|
+
* "services": [
|
|
12
|
+
* { "binding": "WORKFLOWS", "service": "ai-workflows" }
|
|
13
|
+
* ]
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* // worker.ts - consuming service
|
|
17
|
+
* export default {
|
|
18
|
+
* async fetch(request: Request, env: Env) {
|
|
19
|
+
* const service = env.WORKFLOWS.connect()
|
|
20
|
+
* const workflow = service.create('my-workflow', {
|
|
21
|
+
* context: { userId: '123' }
|
|
22
|
+
* })
|
|
23
|
+
* await workflow.emit('Customer.created', { name: 'John' })
|
|
24
|
+
* return Response.json({ status: 'ok' })
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
import { WorkerEntrypoint, RpcTarget, WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
|
|
32
|
+
import { DurableStep, StepContext, DEFAULT_CASCADE_TIMEOUTS, AllTiersFailed, CascadeTimeout, } from './worker/durable-step.js';
|
|
33
|
+
import { Workflow, parseEvent, createTestContext } from './workflow.js';
|
|
34
|
+
import { registerEventHandler, getEventHandlers, clearEventHandlers } from './on.js';
|
|
35
|
+
import { registerScheduleHandler, getScheduleHandlers, clearScheduleHandlers, toCron, intervalToMs, formatInterval, } from './every.js';
|
|
36
|
+
import { send, getEventBus } from './send.js';
|
|
37
|
+
import { WorkflowStateAdapter, } from './worker/state-adapter.js';
|
|
38
|
+
/**
|
|
39
|
+
* Global workflow registry for in-memory workflow instances
|
|
40
|
+
* This enables workflow isolation and persistence across connect() calls in tests
|
|
41
|
+
*/
|
|
42
|
+
const workflowRegistry = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* Counter for generating unique workflow IDs
|
|
45
|
+
*/
|
|
46
|
+
let workflowIdCounter = 0;
|
|
47
|
+
/**
|
|
48
|
+
* Generate a unique workflow ID
|
|
49
|
+
*/
|
|
50
|
+
function generateWorkflowId() {
|
|
51
|
+
return `wf-${++workflowIdCounter}-${Date.now()}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* WorkflowServiceCore - RpcTarget wrapper around workflow functionality
|
|
55
|
+
*
|
|
56
|
+
* Exposes all required methods as RPC-callable methods.
|
|
57
|
+
* This is the core service class that can be instantiated directly.
|
|
58
|
+
*
|
|
59
|
+
* ## State Persistence
|
|
60
|
+
*
|
|
61
|
+
* The service supports optional state persistence via WorkflowStateAdapter.
|
|
62
|
+
* When a database connection is provided, workflow state is automatically
|
|
63
|
+
* persisted across restarts and can be queried.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* // With state persistence
|
|
68
|
+
* import { DB } from 'ai-database'
|
|
69
|
+
* import { WorkflowServiceCore } from 'ai-workflows/worker'
|
|
70
|
+
*
|
|
71
|
+
* const { db } = DB({ WorkflowState: { status: 'string' } })
|
|
72
|
+
* const service = new WorkflowServiceCore(db)
|
|
73
|
+
*
|
|
74
|
+
* // Create and persist workflow
|
|
75
|
+
* const workflow = service.create('order-processor')
|
|
76
|
+
* await service.persistState(workflow.id, { status: 'running' })
|
|
77
|
+
*
|
|
78
|
+
* // Query workflows by status
|
|
79
|
+
* const running = await service.queryByStatus('running')
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export class WorkflowServiceCore extends RpcTarget {
|
|
83
|
+
stateAdapter = null;
|
|
84
|
+
/**
|
|
85
|
+
* Create a WorkflowServiceCore instance
|
|
86
|
+
*
|
|
87
|
+
* @param database - Optional database connection for state persistence
|
|
88
|
+
*/
|
|
89
|
+
constructor(database) {
|
|
90
|
+
super();
|
|
91
|
+
if (database) {
|
|
92
|
+
this.stateAdapter = new WorkflowStateAdapter(database);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ==================== State Persistence ====================
|
|
96
|
+
/**
|
|
97
|
+
* Check if state persistence is enabled
|
|
98
|
+
*
|
|
99
|
+
* @returns True if a database connection was provided
|
|
100
|
+
*/
|
|
101
|
+
hasStatePersistence() {
|
|
102
|
+
return this.stateAdapter !== null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get the state adapter for direct access
|
|
106
|
+
*
|
|
107
|
+
* @returns WorkflowStateAdapter or null if persistence is not enabled
|
|
108
|
+
*/
|
|
109
|
+
getStateAdapter() {
|
|
110
|
+
return this.stateAdapter;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Persist workflow state to the database
|
|
114
|
+
*
|
|
115
|
+
* Saves the current state of a workflow. If the workflow doesn't exist,
|
|
116
|
+
* creates a new record. If it exists, updates the existing record.
|
|
117
|
+
*
|
|
118
|
+
* @param workflowId - The workflow ID
|
|
119
|
+
* @param state - Partial state to save (merged with existing)
|
|
120
|
+
* @throws Error if state persistence is not enabled
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* await service.persistState('wf-123', {
|
|
125
|
+
* status: 'running',
|
|
126
|
+
* currentStep: 'process-payment',
|
|
127
|
+
* context: { orderId: 'order-1' }
|
|
128
|
+
* })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
async persistState(workflowId, state) {
|
|
132
|
+
if (!this.stateAdapter) {
|
|
133
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
134
|
+
}
|
|
135
|
+
await this.stateAdapter.save(workflowId, state);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Load persisted workflow state from the database
|
|
139
|
+
*
|
|
140
|
+
* @param workflowId - The workflow ID to load
|
|
141
|
+
* @returns The persisted state or null if not found
|
|
142
|
+
* @throws Error if state persistence is not enabled
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const state = await service.loadPersistedState('wf-123')
|
|
147
|
+
* if (state) {
|
|
148
|
+
* console.log(`Workflow status: ${state.status}`)
|
|
149
|
+
* console.log(`Current step: ${state.currentStep}`)
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
async loadPersistedState(workflowId) {
|
|
154
|
+
if (!this.stateAdapter) {
|
|
155
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
156
|
+
}
|
|
157
|
+
return this.stateAdapter.load(workflowId);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Save a checkpoint for a workflow step
|
|
161
|
+
*
|
|
162
|
+
* Checkpoints track the execution state of individual steps within a workflow.
|
|
163
|
+
* They enable resumption from the last successful step after failures.
|
|
164
|
+
*
|
|
165
|
+
* @param workflowId - The workflow ID
|
|
166
|
+
* @param stepId - The step ID
|
|
167
|
+
* @param checkpoint - Checkpoint data including status and result
|
|
168
|
+
* @throws Error if state persistence is not enabled
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* await service.saveCheckpoint('wf-123', 'process-payment', {
|
|
173
|
+
* stepId: 'process-payment',
|
|
174
|
+
* status: 'completed',
|
|
175
|
+
* result: { transactionId: 'tx-456' },
|
|
176
|
+
* attempt: 1,
|
|
177
|
+
* completedAt: new Date()
|
|
178
|
+
* })
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
async saveCheckpoint(workflowId, stepId, checkpoint) {
|
|
182
|
+
if (!this.stateAdapter) {
|
|
183
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
184
|
+
}
|
|
185
|
+
await this.stateAdapter.checkpoint(workflowId, stepId, checkpoint);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get a checkpoint for a workflow step
|
|
189
|
+
*
|
|
190
|
+
* @param workflowId - The workflow ID
|
|
191
|
+
* @param stepId - The step ID
|
|
192
|
+
* @returns The checkpoint or null if not found
|
|
193
|
+
* @throws Error if state persistence is not enabled
|
|
194
|
+
*/
|
|
195
|
+
async getCheckpoint(workflowId, stepId) {
|
|
196
|
+
if (!this.stateAdapter) {
|
|
197
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
198
|
+
}
|
|
199
|
+
return this.stateAdapter.getCheckpoint(workflowId, stepId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Update state with optimistic locking
|
|
203
|
+
*
|
|
204
|
+
* Only updates if the current version matches expectedVersion.
|
|
205
|
+
* Use this for concurrent updates to prevent lost writes.
|
|
206
|
+
*
|
|
207
|
+
* @param workflowId - The workflow ID
|
|
208
|
+
* @param expectedVersion - Expected current version
|
|
209
|
+
* @param state - State updates to apply
|
|
210
|
+
* @returns true if updated, false if version mismatch (concurrent modification)
|
|
211
|
+
* @throws Error if state persistence is not enabled
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const state = await service.loadPersistedState('wf-123')
|
|
216
|
+
* const success = await service.updateStateWithVersion(
|
|
217
|
+
* 'wf-123',
|
|
218
|
+
* state.version,
|
|
219
|
+
* { status: 'completed' }
|
|
220
|
+
* )
|
|
221
|
+
* if (!success) {
|
|
222
|
+
* console.log('Concurrent modification detected, retrying...')
|
|
223
|
+
* }
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
async updateStateWithVersion(workflowId, expectedVersion, state) {
|
|
227
|
+
if (!this.stateAdapter) {
|
|
228
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
229
|
+
}
|
|
230
|
+
return this.stateAdapter.updateWithVersion(workflowId, expectedVersion, state);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Query workflows by status
|
|
234
|
+
*
|
|
235
|
+
* @param status - Status to filter by ('pending', 'running', 'completed', 'failed', 'paused')
|
|
236
|
+
* @returns Array of workflows matching the status
|
|
237
|
+
* @throws Error if state persistence is not enabled
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const runningWorkflows = await service.queryByStatus('running')
|
|
242
|
+
* console.log(`${runningWorkflows.length} workflows currently running`)
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
async queryByStatus(status) {
|
|
246
|
+
if (!this.stateAdapter) {
|
|
247
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
248
|
+
}
|
|
249
|
+
return this.stateAdapter.queryByStatus(status);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Query multiple workflows by their IDs
|
|
253
|
+
*
|
|
254
|
+
* @param workflowIds - Array of workflow IDs to query
|
|
255
|
+
* @returns Array of found workflows (non-existent IDs are excluded)
|
|
256
|
+
* @throws Error if state persistence is not enabled
|
|
257
|
+
*/
|
|
258
|
+
async queryByIds(workflowIds) {
|
|
259
|
+
if (!this.stateAdapter) {
|
|
260
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
261
|
+
}
|
|
262
|
+
return this.stateAdapter.queryByIds(workflowIds);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Delete persisted workflow state
|
|
266
|
+
*
|
|
267
|
+
* @param workflowId - The workflow ID to delete
|
|
268
|
+
* @returns true if deleted, false if not found
|
|
269
|
+
* @throws Error if state persistence is not enabled
|
|
270
|
+
*/
|
|
271
|
+
async deletePersistedState(workflowId) {
|
|
272
|
+
if (!this.stateAdapter) {
|
|
273
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
274
|
+
}
|
|
275
|
+
return this.stateAdapter.delete(workflowId);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* List all persisted workflows with pagination
|
|
279
|
+
*
|
|
280
|
+
* @param options - Pagination options (limit, offset)
|
|
281
|
+
* @returns Array of workflows
|
|
282
|
+
* @throws Error if state persistence is not enabled
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* // Get first 10 workflows
|
|
287
|
+
* const workflows = await service.listPersistedWorkflows({ limit: 10, offset: 0 })
|
|
288
|
+
*
|
|
289
|
+
* // Get next page
|
|
290
|
+
* const nextPage = await service.listPersistedWorkflows({ limit: 10, offset: 10 })
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
async listPersistedWorkflows(options) {
|
|
294
|
+
if (!this.stateAdapter) {
|
|
295
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
296
|
+
}
|
|
297
|
+
return this.stateAdapter.listAll(options);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Create a snapshot of current workflow state
|
|
301
|
+
*
|
|
302
|
+
* Snapshots allow point-in-time recovery of workflow state.
|
|
303
|
+
* Useful before executing risky operations.
|
|
304
|
+
*
|
|
305
|
+
* @param workflowId - The workflow ID
|
|
306
|
+
* @param label - Optional human-readable label
|
|
307
|
+
* @returns Snapshot ID
|
|
308
|
+
* @throws Error if state persistence is not enabled or workflow not found
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* const snapshotId = await service.createSnapshot('wf-123', 'before-payment')
|
|
313
|
+
* // ... execute risky operation ...
|
|
314
|
+
* // If something goes wrong:
|
|
315
|
+
* await service.restoreSnapshot('wf-123', snapshotId)
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
async createSnapshot(workflowId, label) {
|
|
319
|
+
if (!this.stateAdapter) {
|
|
320
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
321
|
+
}
|
|
322
|
+
return this.stateAdapter.createSnapshot(workflowId, label);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Restore workflow state from a snapshot
|
|
326
|
+
*
|
|
327
|
+
* @param workflowId - The workflow ID
|
|
328
|
+
* @param snapshotId - The snapshot ID to restore from
|
|
329
|
+
* @throws Error if state persistence is not enabled or snapshot not found
|
|
330
|
+
*/
|
|
331
|
+
async restoreSnapshot(workflowId, snapshotId) {
|
|
332
|
+
if (!this.stateAdapter) {
|
|
333
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
334
|
+
}
|
|
335
|
+
await this.stateAdapter.restoreSnapshot(workflowId, snapshotId);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get all snapshots for a workflow
|
|
339
|
+
*
|
|
340
|
+
* @param workflowId - The workflow ID
|
|
341
|
+
* @returns Array of snapshot metadata
|
|
342
|
+
* @throws Error if state persistence is not enabled
|
|
343
|
+
*/
|
|
344
|
+
async getSnapshots(workflowId) {
|
|
345
|
+
if (!this.stateAdapter) {
|
|
346
|
+
throw new Error('State persistence is not enabled. Provide a database connection to the constructor.');
|
|
347
|
+
}
|
|
348
|
+
return this.stateAdapter.getSnapshots(workflowId);
|
|
349
|
+
}
|
|
350
|
+
// ==================== Workflow Creation ====================
|
|
351
|
+
/**
|
|
352
|
+
* Create a new workflow instance
|
|
353
|
+
*
|
|
354
|
+
* @param name - Optional workflow name
|
|
355
|
+
* @param options - Workflow options (initial context, etc.)
|
|
356
|
+
* @returns Workflow instance info including ID
|
|
357
|
+
*/
|
|
358
|
+
create(name, options = {}) {
|
|
359
|
+
const id = generateWorkflowId();
|
|
360
|
+
const workflowName = name ?? id;
|
|
361
|
+
// Create a basic workflow with empty setup
|
|
362
|
+
// The actual handlers will be registered via registerEvent/registerSchedule
|
|
363
|
+
const instance = Workflow(($) => {
|
|
364
|
+
// Empty setup - handlers registered separately
|
|
365
|
+
}, options);
|
|
366
|
+
// Store in registry
|
|
367
|
+
workflowRegistry.set(id, {
|
|
368
|
+
instance,
|
|
369
|
+
name: workflowName,
|
|
370
|
+
started: false,
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
id,
|
|
374
|
+
name: workflowName,
|
|
375
|
+
state: instance.state,
|
|
376
|
+
eventCount: instance.definition.events.length,
|
|
377
|
+
scheduleCount: instance.definition.schedules.length,
|
|
378
|
+
started: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Create a workflow with a definition function
|
|
383
|
+
* The definition function receives the $ context for registering handlers
|
|
384
|
+
*
|
|
385
|
+
* @param setup - Setup function that receives $ context
|
|
386
|
+
* @param options - Workflow options
|
|
387
|
+
* @returns Workflow instance info
|
|
388
|
+
*/
|
|
389
|
+
createWithSetup(setup, options = {}) {
|
|
390
|
+
const id = generateWorkflowId();
|
|
391
|
+
const instance = Workflow(setup, options);
|
|
392
|
+
const workflowName = instance.definition.name;
|
|
393
|
+
workflowRegistry.set(id, {
|
|
394
|
+
instance,
|
|
395
|
+
name: workflowName,
|
|
396
|
+
started: false,
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
id,
|
|
400
|
+
name: workflowName,
|
|
401
|
+
state: instance.state,
|
|
402
|
+
eventCount: instance.definition.events.length,
|
|
403
|
+
scheduleCount: instance.definition.schedules.length,
|
|
404
|
+
started: false,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// ==================== Workflow Lifecycle ====================
|
|
408
|
+
/**
|
|
409
|
+
* Start a workflow (begin processing schedules)
|
|
410
|
+
*
|
|
411
|
+
* @param workflowId - The workflow ID to start
|
|
412
|
+
*/
|
|
413
|
+
async start(workflowId) {
|
|
414
|
+
const entry = workflowRegistry.get(workflowId);
|
|
415
|
+
if (!entry) {
|
|
416
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
417
|
+
}
|
|
418
|
+
if (entry.started) {
|
|
419
|
+
return; // Already started
|
|
420
|
+
}
|
|
421
|
+
await entry.instance.start();
|
|
422
|
+
entry.started = true;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Stop a workflow
|
|
426
|
+
*
|
|
427
|
+
* @param workflowId - The workflow ID to stop
|
|
428
|
+
*/
|
|
429
|
+
async stop(workflowId) {
|
|
430
|
+
const entry = workflowRegistry.get(workflowId);
|
|
431
|
+
if (!entry) {
|
|
432
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
433
|
+
}
|
|
434
|
+
await entry.instance.stop();
|
|
435
|
+
entry.started = false;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Destroy a workflow and clean up resources
|
|
439
|
+
*
|
|
440
|
+
* @param workflowId - The workflow ID to destroy
|
|
441
|
+
*/
|
|
442
|
+
async destroy(workflowId) {
|
|
443
|
+
const entry = workflowRegistry.get(workflowId);
|
|
444
|
+
if (!entry) {
|
|
445
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
446
|
+
}
|
|
447
|
+
await entry.instance.destroy();
|
|
448
|
+
workflowRegistry.delete(workflowId);
|
|
449
|
+
}
|
|
450
|
+
// ==================== Event Emission ====================
|
|
451
|
+
/**
|
|
452
|
+
* Send an event to a workflow
|
|
453
|
+
*
|
|
454
|
+
* @param workflowId - The workflow ID
|
|
455
|
+
* @param event - Event name in Noun.event format (e.g., 'Customer.created')
|
|
456
|
+
* @param data - Event data
|
|
457
|
+
* @returns Event ID
|
|
458
|
+
*/
|
|
459
|
+
emit(workflowId, event, data) {
|
|
460
|
+
const entry = workflowRegistry.get(workflowId);
|
|
461
|
+
if (!entry) {
|
|
462
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
463
|
+
}
|
|
464
|
+
return entry.instance.$.send(event, data);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Send an event using the global event bus (for standalone/testing)
|
|
468
|
+
*
|
|
469
|
+
* @param event - Event name
|
|
470
|
+
* @param data - Event data
|
|
471
|
+
*/
|
|
472
|
+
async sendGlobal(event, data) {
|
|
473
|
+
await send(event, data);
|
|
474
|
+
}
|
|
475
|
+
// ==================== State Management ====================
|
|
476
|
+
/**
|
|
477
|
+
* Get workflow state
|
|
478
|
+
*
|
|
479
|
+
* @param workflowId - The workflow ID
|
|
480
|
+
* @returns Current workflow state
|
|
481
|
+
*/
|
|
482
|
+
getState(workflowId) {
|
|
483
|
+
const entry = workflowRegistry.get(workflowId);
|
|
484
|
+
if (!entry) {
|
|
485
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
486
|
+
}
|
|
487
|
+
return entry.instance.$.getState();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Set a value in workflow context
|
|
491
|
+
*
|
|
492
|
+
* @param workflowId - The workflow ID
|
|
493
|
+
* @param key - Context key
|
|
494
|
+
* @param value - Value to set
|
|
495
|
+
*/
|
|
496
|
+
setState(workflowId, key, value) {
|
|
497
|
+
const entry = workflowRegistry.get(workflowId);
|
|
498
|
+
if (!entry) {
|
|
499
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
500
|
+
}
|
|
501
|
+
entry.instance.$.set(key, value);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get a value from workflow context
|
|
505
|
+
*
|
|
506
|
+
* @param workflowId - The workflow ID
|
|
507
|
+
* @param key - Context key
|
|
508
|
+
* @returns Value or undefined
|
|
509
|
+
*/
|
|
510
|
+
getValue(workflowId, key) {
|
|
511
|
+
const entry = workflowRegistry.get(workflowId);
|
|
512
|
+
if (!entry) {
|
|
513
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
514
|
+
}
|
|
515
|
+
return entry.instance.$.get(key);
|
|
516
|
+
}
|
|
517
|
+
// ==================== Workflow Info ====================
|
|
518
|
+
/**
|
|
519
|
+
* Get workflow info
|
|
520
|
+
*
|
|
521
|
+
* @param workflowId - The workflow ID
|
|
522
|
+
* @returns Workflow instance info
|
|
523
|
+
*/
|
|
524
|
+
get(workflowId) {
|
|
525
|
+
const entry = workflowRegistry.get(workflowId);
|
|
526
|
+
if (!entry) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
id: workflowId,
|
|
531
|
+
name: entry.name,
|
|
532
|
+
state: entry.instance.state,
|
|
533
|
+
eventCount: entry.instance.definition.events.length,
|
|
534
|
+
scheduleCount: entry.instance.definition.schedules.length,
|
|
535
|
+
started: entry.started,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* List all workflow IDs
|
|
540
|
+
*
|
|
541
|
+
* @returns Array of workflow IDs
|
|
542
|
+
*/
|
|
543
|
+
list() {
|
|
544
|
+
return Array.from(workflowRegistry.keys());
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Check if a workflow exists
|
|
548
|
+
*
|
|
549
|
+
* @param workflowId - The workflow ID
|
|
550
|
+
* @returns True if workflow exists
|
|
551
|
+
*/
|
|
552
|
+
has(workflowId) {
|
|
553
|
+
return workflowRegistry.has(workflowId);
|
|
554
|
+
}
|
|
555
|
+
// ==================== Global Event Handlers ====================
|
|
556
|
+
/**
|
|
557
|
+
* Register a global event handler (for standalone usage)
|
|
558
|
+
*
|
|
559
|
+
* @param noun - Noun (e.g., 'Customer')
|
|
560
|
+
* @param event - Event name (e.g., 'created')
|
|
561
|
+
* @param handler - Handler function
|
|
562
|
+
*/
|
|
563
|
+
registerGlobalEvent(noun, event, handler) {
|
|
564
|
+
registerEventHandler(noun, event, handler);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Get all global event handlers
|
|
568
|
+
*
|
|
569
|
+
* @returns Array of event registrations
|
|
570
|
+
*/
|
|
571
|
+
getGlobalEventHandlers() {
|
|
572
|
+
return getEventHandlers();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Clear all global event handlers
|
|
576
|
+
*/
|
|
577
|
+
clearGlobalEventHandlers() {
|
|
578
|
+
clearEventHandlers();
|
|
579
|
+
}
|
|
580
|
+
// ==================== Global Schedule Handlers ====================
|
|
581
|
+
/**
|
|
582
|
+
* Register a global schedule handler (for standalone usage)
|
|
583
|
+
*
|
|
584
|
+
* @param interval - Schedule interval
|
|
585
|
+
* @param handler - Handler function
|
|
586
|
+
*/
|
|
587
|
+
registerGlobalSchedule(interval, handler) {
|
|
588
|
+
registerScheduleHandler(interval, handler);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get all global schedule handlers
|
|
592
|
+
*
|
|
593
|
+
* @returns Array of schedule registrations
|
|
594
|
+
*/
|
|
595
|
+
getGlobalScheduleHandlers() {
|
|
596
|
+
return getScheduleHandlers();
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Clear all global schedule handlers
|
|
600
|
+
*/
|
|
601
|
+
clearGlobalScheduleHandlers() {
|
|
602
|
+
clearScheduleHandlers();
|
|
603
|
+
}
|
|
604
|
+
// ==================== Utilities ====================
|
|
605
|
+
/**
|
|
606
|
+
* Parse an event string into noun and event
|
|
607
|
+
*
|
|
608
|
+
* @param event - Event string (e.g., 'Customer.created')
|
|
609
|
+
* @returns Parsed event or null if invalid
|
|
610
|
+
*/
|
|
611
|
+
parseEvent(event) {
|
|
612
|
+
return parseEvent(event);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Convert a natural language schedule description to cron expression
|
|
616
|
+
*
|
|
617
|
+
* @param description - Natural language description (e.g., 'every hour', 'every Monday')
|
|
618
|
+
* @returns Cron expression
|
|
619
|
+
*/
|
|
620
|
+
async toCron(description) {
|
|
621
|
+
return toCron(description);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Convert a schedule interval to milliseconds
|
|
625
|
+
*
|
|
626
|
+
* @param interval - Schedule interval
|
|
627
|
+
* @returns Milliseconds
|
|
628
|
+
*/
|
|
629
|
+
intervalToMs(interval) {
|
|
630
|
+
return intervalToMs(interval);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Format a schedule interval as a human-readable string
|
|
634
|
+
*
|
|
635
|
+
* @param interval - Schedule interval
|
|
636
|
+
* @returns Formatted string
|
|
637
|
+
*/
|
|
638
|
+
formatInterval(interval) {
|
|
639
|
+
return formatInterval(interval);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Create an isolated test context
|
|
643
|
+
*
|
|
644
|
+
* @returns Test context with emittedEvents tracking
|
|
645
|
+
*/
|
|
646
|
+
createTestContext() {
|
|
647
|
+
return createTestContext();
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Clear all workflows (for testing)
|
|
651
|
+
*/
|
|
652
|
+
clear() {
|
|
653
|
+
for (const [id, entry] of workflowRegistry) {
|
|
654
|
+
entry.instance.destroy().catch(() => { });
|
|
655
|
+
}
|
|
656
|
+
workflowRegistry.clear();
|
|
657
|
+
workflowIdCounter = 0;
|
|
658
|
+
}
|
|
659
|
+
// ==================== WorkflowBuilder Integration ====================
|
|
660
|
+
/**
|
|
661
|
+
* Register a built workflow from WorkflowBuilder
|
|
662
|
+
*
|
|
663
|
+
* @param workflow - The built workflow from WorkflowBuilder.build()
|
|
664
|
+
* @returns Registration info with unique ID
|
|
665
|
+
*/
|
|
666
|
+
registerWorkflow(workflow) {
|
|
667
|
+
const id = generateWorkflowId();
|
|
668
|
+
// Create references to steps for closure
|
|
669
|
+
const steps = workflow.steps;
|
|
670
|
+
const eventTriggers = workflow.triggers.events;
|
|
671
|
+
const scheduleTriggers = workflow.triggers.schedules;
|
|
672
|
+
// Parse schedule intervals outside the closure
|
|
673
|
+
const scheduleIntervals = scheduleTriggers.map((trigger) => ({
|
|
674
|
+
trigger,
|
|
675
|
+
interval: this.parseScheduleToInterval(trigger.schedule, trigger.time, trigger.timezone),
|
|
676
|
+
}));
|
|
677
|
+
// Create a workflow instance with the built workflow's configuration
|
|
678
|
+
const instance = Workflow(($) => {
|
|
679
|
+
// Register event handlers from the built workflow using $.on proxy
|
|
680
|
+
for (const trigger of eventTriggers) {
|
|
681
|
+
const [noun, event] = trigger.event.split('.');
|
|
682
|
+
if (noun && event) {
|
|
683
|
+
const step = steps.find((s) => s.name === trigger.stepName);
|
|
684
|
+
if (step) {
|
|
685
|
+
// Use $.on[noun][event](handler) to register on the workflow instance
|
|
686
|
+
const nounProxy = $.on[noun];
|
|
687
|
+
if (nounProxy && typeof nounProxy[event] === 'function') {
|
|
688
|
+
nounProxy[event](async (data, ctx) => {
|
|
689
|
+
// Apply filter if present
|
|
690
|
+
if (trigger.filter && !trigger.filter(data)) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
return step.fn(data, ctx);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Register schedule handlers from the built workflow using $.every
|
|
700
|
+
for (const { trigger, interval } of scheduleIntervals) {
|
|
701
|
+
const step = steps.find((s) => s.name === trigger.stepName);
|
|
702
|
+
if (step) {
|
|
703
|
+
// Use appropriate $.every method based on interval type
|
|
704
|
+
if (interval.type === 'second') {
|
|
705
|
+
if (interval.value && interval.value > 1) {
|
|
706
|
+
$.every.seconds(interval.value)(async (ctx) => step.fn(undefined, ctx));
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
$.every.second(async (ctx) => step.fn(undefined, ctx));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else if (interval.type === 'minute') {
|
|
713
|
+
if (interval.value && interval.value > 1) {
|
|
714
|
+
$.every.minutes(interval.value)(async (ctx) => step.fn(undefined, ctx));
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
$.every.minute(async (ctx) => step.fn(undefined, ctx));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
else if (interval.type === 'hour') {
|
|
721
|
+
if (interval.value && interval.value > 1) {
|
|
722
|
+
$.every.hours(interval.value)(async (ctx) => step.fn(undefined, ctx));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
$.every.hour(async (ctx) => step.fn(undefined, ctx));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
else if (interval.type === 'day') {
|
|
729
|
+
$.every.day(async (ctx) => step.fn(undefined, ctx));
|
|
730
|
+
}
|
|
731
|
+
else if (interval.type === 'week') {
|
|
732
|
+
$.every.week(async (ctx) => step.fn(undefined, ctx));
|
|
733
|
+
}
|
|
734
|
+
else if (interval.type === 'natural') {
|
|
735
|
+
$.every(interval.description, async (ctx) => step.fn(undefined, ctx));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}, {});
|
|
740
|
+
// Store in registry
|
|
741
|
+
workflowRegistry.set(id, {
|
|
742
|
+
instance,
|
|
743
|
+
name: workflow.name,
|
|
744
|
+
started: false,
|
|
745
|
+
});
|
|
746
|
+
return { id };
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Parse schedule string to ScheduleInterval
|
|
750
|
+
*/
|
|
751
|
+
parseScheduleToInterval(schedule, time, timezone) {
|
|
752
|
+
// Handle milliseconds for testing
|
|
753
|
+
if (schedule.endsWith('ms')) {
|
|
754
|
+
const value = parseInt(schedule.slice(0, -2), 10);
|
|
755
|
+
return { type: 'second', value: Math.max(1, Math.ceil(value / 1000)) };
|
|
756
|
+
}
|
|
757
|
+
// Handle common intervals
|
|
758
|
+
const scheduleMap = {
|
|
759
|
+
second: { type: 'second' },
|
|
760
|
+
minute: { type: 'minute' },
|
|
761
|
+
hour: { type: 'hour' },
|
|
762
|
+
day: { type: 'day' },
|
|
763
|
+
week: { type: 'week' },
|
|
764
|
+
};
|
|
765
|
+
if (schedule in scheduleMap) {
|
|
766
|
+
const result = scheduleMap[schedule];
|
|
767
|
+
if (result) {
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Handle numeric intervals like "5 minutes"
|
|
772
|
+
const match = schedule.match(/^(\d+)\s*(second|minute|hour|day|week)s?$/);
|
|
773
|
+
if (match && match[1] !== undefined && match[2] !== undefined) {
|
|
774
|
+
const value = parseInt(match[1], 10);
|
|
775
|
+
const type = match[2];
|
|
776
|
+
return { type, value };
|
|
777
|
+
}
|
|
778
|
+
// Handle day names and cron expressions as natural language
|
|
779
|
+
return { type: 'natural', description: schedule };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* WorkflowService - WorkerEntrypoint for RPC access
|
|
784
|
+
*
|
|
785
|
+
* Provides `connect()` method that returns an RpcTarget service
|
|
786
|
+
* with all workflow methods.
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* ```typescript
|
|
790
|
+
* // In consuming worker
|
|
791
|
+
* const service = env.WORKFLOWS.connect()
|
|
792
|
+
* const workflow = service.create('my-workflow')
|
|
793
|
+
* await service.start(workflow.id)
|
|
794
|
+
* service.emit(workflow.id, 'Customer.created', { name: 'John' })
|
|
795
|
+
* ```
|
|
796
|
+
*/
|
|
797
|
+
export class WorkflowService extends WorkerEntrypoint {
|
|
798
|
+
/**
|
|
799
|
+
* Connect to the workflow service and get an RPC-enabled service
|
|
800
|
+
*
|
|
801
|
+
* @returns WorkflowServiceCore instance for RPC calls
|
|
802
|
+
*/
|
|
803
|
+
connect() {
|
|
804
|
+
return new WorkflowServiceCore();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Track retry attempts across workflow instances (for testing retry behavior)
|
|
809
|
+
*/
|
|
810
|
+
const retryAttemptTracker = new Map();
|
|
811
|
+
/**
|
|
812
|
+
* Track execution history for state persistence tests
|
|
813
|
+
*/
|
|
814
|
+
const executionHistory = new Map();
|
|
815
|
+
/**
|
|
816
|
+
* Track state sharing between steps
|
|
817
|
+
*/
|
|
818
|
+
const sharedState = new Map();
|
|
819
|
+
/**
|
|
820
|
+
* TestWorkflow - A Cloudflare Workflow class for testing DurableStep
|
|
821
|
+
*
|
|
822
|
+
* This workflow handles various test scenarios based on the workflow instance ID.
|
|
823
|
+
* Each test creates a workflow instance with a specific ID pattern, and this
|
|
824
|
+
* workflow executes the appropriate test scenario.
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```typescript
|
|
828
|
+
* // In wrangler.jsonc
|
|
829
|
+
* {
|
|
830
|
+
* "workflows": [{
|
|
831
|
+
* "name": "test-workflow",
|
|
832
|
+
* "binding": "WORKFLOW",
|
|
833
|
+
* "class_name": "TestWorkflow"
|
|
834
|
+
* }]
|
|
835
|
+
* }
|
|
836
|
+
* ```
|
|
837
|
+
*/
|
|
838
|
+
export class TestWorkflow extends WorkflowEntrypoint {
|
|
839
|
+
/**
|
|
840
|
+
* Main workflow entry point
|
|
841
|
+
* Routes to different test scenarios based on the workflow instance ID
|
|
842
|
+
*/
|
|
843
|
+
async run(event, step) {
|
|
844
|
+
// Get the workflow instance ID from the event or use a default
|
|
845
|
+
// WorkflowEvent provides instanceId property
|
|
846
|
+
const instanceId = event.instanceId ?? 'default';
|
|
847
|
+
// Route to the appropriate test scenario based on the instance ID prefix
|
|
848
|
+
if (instanceId.startsWith('exec-test')) {
|
|
849
|
+
return this.execTest(step);
|
|
850
|
+
}
|
|
851
|
+
else if (instanceId.startsWith('durability-test')) {
|
|
852
|
+
return this.durabilityTest(step);
|
|
853
|
+
}
|
|
854
|
+
else if (instanceId.startsWith('config-test')) {
|
|
855
|
+
return this.configTest(step);
|
|
856
|
+
}
|
|
857
|
+
else if (instanceId.startsWith('result-test')) {
|
|
858
|
+
return this.resultTest(step);
|
|
859
|
+
}
|
|
860
|
+
else if (instanceId.startsWith('error-test')) {
|
|
861
|
+
return this.errorTest(step);
|
|
862
|
+
}
|
|
863
|
+
else if (instanceId.startsWith('typed-test')) {
|
|
864
|
+
return this.typedTest(step);
|
|
865
|
+
}
|
|
866
|
+
else if (instanceId.startsWith('void-input-test')) {
|
|
867
|
+
return this.voidInputTest(step);
|
|
868
|
+
}
|
|
869
|
+
else if (instanceId.startsWith('ctx-test')) {
|
|
870
|
+
return this.ctxTest(step);
|
|
871
|
+
}
|
|
872
|
+
else if (instanceId.startsWith('metadata-test')) {
|
|
873
|
+
return this.metadataTest(step);
|
|
874
|
+
}
|
|
875
|
+
else if (instanceId.startsWith('side-effect-test')) {
|
|
876
|
+
return this.sideEffectTest(step);
|
|
877
|
+
}
|
|
878
|
+
else if (instanceId.startsWith('retry-config-test')) {
|
|
879
|
+
return this.retryConfigTest(step);
|
|
880
|
+
}
|
|
881
|
+
else if (instanceId.startsWith('side-effect-error-test')) {
|
|
882
|
+
return this.sideEffectErrorTest(step);
|
|
883
|
+
}
|
|
884
|
+
else if (instanceId.startsWith('sequential-test')) {
|
|
885
|
+
return this.sequentialTest(step);
|
|
886
|
+
}
|
|
887
|
+
else if (instanceId.startsWith('sleep-test')) {
|
|
888
|
+
return this.sleepTest(step);
|
|
889
|
+
}
|
|
890
|
+
else if (instanceId.startsWith('multi-sleep-test')) {
|
|
891
|
+
return this.multiSleepTest(step);
|
|
892
|
+
}
|
|
893
|
+
else if (instanceId.startsWith('sleep-until-test')) {
|
|
894
|
+
return this.sleepUntilTest(step);
|
|
895
|
+
}
|
|
896
|
+
else if (instanceId.startsWith('timestamp-sleep-test')) {
|
|
897
|
+
return this.timestampSleepTest(step);
|
|
898
|
+
}
|
|
899
|
+
else if (instanceId.startsWith('step-id-test')) {
|
|
900
|
+
return this.stepIdTest(step);
|
|
901
|
+
}
|
|
902
|
+
else if (instanceId.startsWith('attempt-test')) {
|
|
903
|
+
return this.attemptTest(step);
|
|
904
|
+
}
|
|
905
|
+
else if (instanceId.startsWith('retries-limit-test')) {
|
|
906
|
+
return this.retriesLimitTest(step);
|
|
907
|
+
}
|
|
908
|
+
else if (instanceId.startsWith('no-retries-test')) {
|
|
909
|
+
return this.noRetriesTest(step);
|
|
910
|
+
}
|
|
911
|
+
else if (instanceId.startsWith('retry-behavior-test')) {
|
|
912
|
+
return this.retryBehaviorTest(step, instanceId);
|
|
913
|
+
}
|
|
914
|
+
else if (instanceId.startsWith('timeout-test')) {
|
|
915
|
+
return this.timeoutTest(step);
|
|
916
|
+
}
|
|
917
|
+
else if (instanceId.startsWith('exp-backoff-test')) {
|
|
918
|
+
return this.expBackoffTest(step);
|
|
919
|
+
}
|
|
920
|
+
else if (instanceId.startsWith('linear-backoff-test')) {
|
|
921
|
+
return this.linearBackoffTest(step);
|
|
922
|
+
}
|
|
923
|
+
else if (instanceId.startsWith('constant-backoff-test')) {
|
|
924
|
+
return this.constantBackoffTest(step);
|
|
925
|
+
}
|
|
926
|
+
else if (instanceId.startsWith('no-retry-error-test')) {
|
|
927
|
+
return this.noRetryErrorTest(step);
|
|
928
|
+
}
|
|
929
|
+
else if (instanceId.startsWith('sequential-steps-test')) {
|
|
930
|
+
return this.sequentialStepsTest(step);
|
|
931
|
+
}
|
|
932
|
+
else if (instanceId.startsWith('factory-test')) {
|
|
933
|
+
return this.factoryTest(step);
|
|
934
|
+
}
|
|
935
|
+
else if (instanceId.startsWith('parallel-test')) {
|
|
936
|
+
return this.parallelTest(step);
|
|
937
|
+
}
|
|
938
|
+
else if (instanceId.startsWith('persist-before-test')) {
|
|
939
|
+
return this.persistBeforeTest(step, instanceId);
|
|
940
|
+
}
|
|
941
|
+
else if (instanceId.startsWith('persist-after-test')) {
|
|
942
|
+
return this.persistAfterTest(step, instanceId);
|
|
943
|
+
}
|
|
944
|
+
else if (instanceId.startsWith('resume-test')) {
|
|
945
|
+
return this.resumeTest(step, instanceId);
|
|
946
|
+
}
|
|
947
|
+
else if (instanceId.startsWith('history-test')) {
|
|
948
|
+
return this.historyTest(step, instanceId);
|
|
949
|
+
}
|
|
950
|
+
else if (instanceId.startsWith('graceful-timeout-test')) {
|
|
951
|
+
return this.gracefulTimeoutTest(step);
|
|
952
|
+
}
|
|
953
|
+
else if (instanceId.startsWith('after-timeout-test')) {
|
|
954
|
+
return this.afterTimeoutTest(step);
|
|
955
|
+
}
|
|
956
|
+
else if (instanceId.startsWith('per-step-timeout-test')) {
|
|
957
|
+
return this.perStepTimeoutTest(step);
|
|
958
|
+
}
|
|
959
|
+
else if (instanceId.startsWith('service-integration-test')) {
|
|
960
|
+
return this.serviceIntegrationTest(step);
|
|
961
|
+
}
|
|
962
|
+
else if (instanceId.startsWith('context-access-test')) {
|
|
963
|
+
return this.contextAccessTest(step);
|
|
964
|
+
}
|
|
965
|
+
else if (instanceId.startsWith('state-sharing-test')) {
|
|
966
|
+
return this.stateSharingTest(step, instanceId);
|
|
967
|
+
}
|
|
968
|
+
else if (instanceId.startsWith('empty-input-test')) {
|
|
969
|
+
return this.emptyInputTest(step);
|
|
970
|
+
}
|
|
971
|
+
else if (instanceId.startsWith('large-input-test')) {
|
|
972
|
+
return this.largeInputTest(step);
|
|
973
|
+
}
|
|
974
|
+
else if (instanceId.startsWith('large-output-test')) {
|
|
975
|
+
return this.largeOutputTest(step);
|
|
976
|
+
}
|
|
977
|
+
else if (instanceId.startsWith('nested-do-test')) {
|
|
978
|
+
return this.nestedDoTest(step);
|
|
979
|
+
}
|
|
980
|
+
else if (instanceId.startsWith('concurrent-test')) {
|
|
981
|
+
return this.concurrentTest(step, instanceId);
|
|
982
|
+
}
|
|
983
|
+
else if (instanceId.startsWith('cascade-order-test')) {
|
|
984
|
+
return this.cascadeOrderTest(step);
|
|
985
|
+
}
|
|
986
|
+
else if (instanceId.startsWith('cascade-shortcircuit-test')) {
|
|
987
|
+
return this.cascadeShortcircuitTest(step);
|
|
988
|
+
}
|
|
989
|
+
else if (instanceId.startsWith('cascade-escalate-test')) {
|
|
990
|
+
return this.cascadeEscalateTest(step);
|
|
991
|
+
}
|
|
992
|
+
else if (instanceId.startsWith('cascade-skip-test')) {
|
|
993
|
+
return this.cascadeSkipTest(step);
|
|
994
|
+
}
|
|
995
|
+
else if (instanceId.startsWith('ai-human-fallback-test')) {
|
|
996
|
+
return this.aiHumanFallbackTest(step);
|
|
997
|
+
}
|
|
998
|
+
else if (instanceId.startsWith('ai-error-context-test')) {
|
|
999
|
+
return this.aiErrorContextTest(step);
|
|
1000
|
+
}
|
|
1001
|
+
else if (instanceId.startsWith('ai-reasoning-test')) {
|
|
1002
|
+
return this.aiReasoningTest(step);
|
|
1003
|
+
}
|
|
1004
|
+
else if (instanceId.startsWith('custom-escalation-test')) {
|
|
1005
|
+
return this.customEscalationTest(step);
|
|
1006
|
+
}
|
|
1007
|
+
else if (instanceId.startsWith('fast-slow-model-test')) {
|
|
1008
|
+
return this.fastSlowModelTest(step);
|
|
1009
|
+
}
|
|
1010
|
+
else if (instanceId.startsWith('model-cascade-test')) {
|
|
1011
|
+
return this.modelCascadeTest(step);
|
|
1012
|
+
}
|
|
1013
|
+
else if (instanceId.startsWith('custom-model-order-test')) {
|
|
1014
|
+
return this.customModelOrderTest(step);
|
|
1015
|
+
}
|
|
1016
|
+
else if (instanceId.startsWith('model-timeout-test')) {
|
|
1017
|
+
return this.modelTimeoutTest(step);
|
|
1018
|
+
}
|
|
1019
|
+
else if (instanceId.startsWith('ai-gateway-cache-test')) {
|
|
1020
|
+
return this.aiGatewayCacheTest(step);
|
|
1021
|
+
}
|
|
1022
|
+
else if (instanceId.startsWith('tier-timeout-config-test')) {
|
|
1023
|
+
return this.tierTimeoutConfigTest(step);
|
|
1024
|
+
}
|
|
1025
|
+
else if (instanceId.startsWith('default-timeout-test')) {
|
|
1026
|
+
return this.defaultTimeoutTest(step);
|
|
1027
|
+
}
|
|
1028
|
+
else if (instanceId.startsWith('timeout-escalation-test')) {
|
|
1029
|
+
return this.timeoutEscalationTest(step);
|
|
1030
|
+
}
|
|
1031
|
+
else if (instanceId.startsWith('timeout-record-test')) {
|
|
1032
|
+
return this.timeoutRecordTest(step);
|
|
1033
|
+
}
|
|
1034
|
+
else if (instanceId.startsWith('total-timeout-test')) {
|
|
1035
|
+
return this.totalTimeoutTest(step);
|
|
1036
|
+
}
|
|
1037
|
+
else if (instanceId.startsWith('return-success-test')) {
|
|
1038
|
+
return this.returnSuccessTest(step);
|
|
1039
|
+
}
|
|
1040
|
+
else if (instanceId.startsWith('throw-failure-test')) {
|
|
1041
|
+
return this.throwFailureTest(step);
|
|
1042
|
+
}
|
|
1043
|
+
else if (instanceId.startsWith('custom-success-test')) {
|
|
1044
|
+
return this.customSuccessTest(step);
|
|
1045
|
+
}
|
|
1046
|
+
else if (instanceId.startsWith('partial-success-test')) {
|
|
1047
|
+
return this.partialSuccessTest(step);
|
|
1048
|
+
}
|
|
1049
|
+
else if (instanceId.startsWith('retry-before-escalate-test')) {
|
|
1050
|
+
return this.retryBeforeEscalateTest(step);
|
|
1051
|
+
}
|
|
1052
|
+
else if (instanceId.startsWith('result-accumulate-test')) {
|
|
1053
|
+
return this.resultAccumulateTest(step);
|
|
1054
|
+
}
|
|
1055
|
+
else if (instanceId.startsWith('result-merge-test')) {
|
|
1056
|
+
return this.resultMergeTest(step);
|
|
1057
|
+
}
|
|
1058
|
+
else if (instanceId.startsWith('individual-results-test')) {
|
|
1059
|
+
return this.individualResultsTest(step);
|
|
1060
|
+
}
|
|
1061
|
+
else if (instanceId.startsWith('custom-merger-test')) {
|
|
1062
|
+
return this.customMergerTest(step);
|
|
1063
|
+
}
|
|
1064
|
+
else if (instanceId.startsWith('tier-metadata-test')) {
|
|
1065
|
+
return this.tierMetadataTest(step);
|
|
1066
|
+
}
|
|
1067
|
+
else if (instanceId.startsWith('error-propagate-test')) {
|
|
1068
|
+
return this.errorPropagateTest(step);
|
|
1069
|
+
}
|
|
1070
|
+
else if (instanceId.startsWith('error-accumulate-test')) {
|
|
1071
|
+
return this.errorAccumulateTest(step);
|
|
1072
|
+
}
|
|
1073
|
+
else if (instanceId.startsWith('all-tiers-fail-test')) {
|
|
1074
|
+
return this.allTiersFailTest(step);
|
|
1075
|
+
}
|
|
1076
|
+
else if (instanceId.startsWith('error-history-test')) {
|
|
1077
|
+
return this.errorHistoryTest(step);
|
|
1078
|
+
}
|
|
1079
|
+
else if (instanceId.startsWith('custom-error-handler-test')) {
|
|
1080
|
+
return this.customErrorHandlerTest(step);
|
|
1081
|
+
}
|
|
1082
|
+
else if (instanceId.startsWith('error-transform-test')) {
|
|
1083
|
+
return this.errorTransformTest(step);
|
|
1084
|
+
}
|
|
1085
|
+
else if (instanceId.startsWith('durable-checkpoint-test')) {
|
|
1086
|
+
return this.durableCheckpointTest(step);
|
|
1087
|
+
}
|
|
1088
|
+
else if (instanceId.startsWith('cascade-resume-test')) {
|
|
1089
|
+
return this.cascadeResumeTest(step);
|
|
1090
|
+
}
|
|
1091
|
+
else if (instanceId.startsWith('durable-io-test')) {
|
|
1092
|
+
return this.durableIoTest(step);
|
|
1093
|
+
}
|
|
1094
|
+
else if (instanceId.startsWith('cascade-snapshot-test')) {
|
|
1095
|
+
return this.cascadeSnapshotTest(step);
|
|
1096
|
+
}
|
|
1097
|
+
else if (instanceId.startsWith('audit-who-test')) {
|
|
1098
|
+
return this.auditWhoTest(step);
|
|
1099
|
+
}
|
|
1100
|
+
else if (instanceId.startsWith('audit-what-test')) {
|
|
1101
|
+
return this.auditWhatTest(step);
|
|
1102
|
+
}
|
|
1103
|
+
else if (instanceId.startsWith('audit-when-test')) {
|
|
1104
|
+
return this.auditWhenTest(step);
|
|
1105
|
+
}
|
|
1106
|
+
else if (instanceId.startsWith('audit-where-test')) {
|
|
1107
|
+
return this.auditWhereTest(step);
|
|
1108
|
+
}
|
|
1109
|
+
else if (instanceId.startsWith('audit-why-test')) {
|
|
1110
|
+
return this.auditWhyTest(step);
|
|
1111
|
+
}
|
|
1112
|
+
else if (instanceId.startsWith('audit-how-test')) {
|
|
1113
|
+
return this.auditHowTest(step);
|
|
1114
|
+
}
|
|
1115
|
+
else if (instanceId.startsWith('audit-persist-test')) {
|
|
1116
|
+
return this.auditPersistTest(step);
|
|
1117
|
+
}
|
|
1118
|
+
else if (instanceId.startsWith('ai-gateway-binding-test')) {
|
|
1119
|
+
return this.aiGatewayBindingTest(step);
|
|
1120
|
+
}
|
|
1121
|
+
else if (instanceId.startsWith('ai-gateway-caching-test')) {
|
|
1122
|
+
return this.aiGatewayCachingTest(step);
|
|
1123
|
+
}
|
|
1124
|
+
else if (instanceId.startsWith('ai-context-test')) {
|
|
1125
|
+
return this.aiContextTest(step);
|
|
1126
|
+
}
|
|
1127
|
+
else if (instanceId.startsWith('ai-gateway-error-test')) {
|
|
1128
|
+
return this.aiGatewayErrorTest(step);
|
|
1129
|
+
}
|
|
1130
|
+
else if (instanceId.startsWith('cascade-context-test')) {
|
|
1131
|
+
return this.cascadeContextTest(step);
|
|
1132
|
+
}
|
|
1133
|
+
else if (instanceId.startsWith('fivewh-events-test')) {
|
|
1134
|
+
return this.fivewhEventsTest(step);
|
|
1135
|
+
}
|
|
1136
|
+
else if (instanceId.startsWith('metrics-test')) {
|
|
1137
|
+
return this.metricsTest(step);
|
|
1138
|
+
}
|
|
1139
|
+
// Default test - just return a simple value
|
|
1140
|
+
return { value: 42 };
|
|
1141
|
+
}
|
|
1142
|
+
// ============================================================================
|
|
1143
|
+
// Test Scenario Implementations
|
|
1144
|
+
// ============================================================================
|
|
1145
|
+
/**
|
|
1146
|
+
* Basic execution test - verifies DurableStep executes and returns value
|
|
1147
|
+
*/
|
|
1148
|
+
async execTest(step) {
|
|
1149
|
+
const durableStep = new DurableStep('compute', async () => {
|
|
1150
|
+
return { value: 42 };
|
|
1151
|
+
});
|
|
1152
|
+
return durableStep.run(step, undefined);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Durability test - verifies step.do() is called with correct name
|
|
1156
|
+
*/
|
|
1157
|
+
async durabilityTest(step) {
|
|
1158
|
+
const durableStep = new DurableStep('durable-action', async () => {
|
|
1159
|
+
return { stepName: 'durable-action' };
|
|
1160
|
+
});
|
|
1161
|
+
return durableStep.run(step, undefined);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Config test - verifies configuration is passed to step.do()
|
|
1165
|
+
*/
|
|
1166
|
+
async configTest(step) {
|
|
1167
|
+
const durableStep = new DurableStep('configured-step', { retries: { limit: 3, delay: '1 second' }, timeout: '30 seconds' }, async () => {
|
|
1168
|
+
return { configApplied: true };
|
|
1169
|
+
});
|
|
1170
|
+
return durableStep.run(step, undefined);
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Result test - verifies correct values are returned
|
|
1174
|
+
*/
|
|
1175
|
+
async resultTest(step) {
|
|
1176
|
+
const durableStep = new DurableStep('math-step', async () => {
|
|
1177
|
+
return { sum: 10, product: 21 };
|
|
1178
|
+
});
|
|
1179
|
+
return durableStep.run(step, undefined);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Error test - verifies errors are propagated
|
|
1183
|
+
* Explicitly disable retries so error propagates immediately
|
|
1184
|
+
*/
|
|
1185
|
+
async errorTest(step) {
|
|
1186
|
+
const durableStep = new DurableStep('failing-step', { retries: { limit: 0 } }, async () => {
|
|
1187
|
+
throw new Error('Step execution failed');
|
|
1188
|
+
});
|
|
1189
|
+
return durableStep.run(step, undefined);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Typed test - verifies generic types work correctly
|
|
1193
|
+
*/
|
|
1194
|
+
async typedTest(step) {
|
|
1195
|
+
const durableStep = new DurableStep('process-order', async (input) => {
|
|
1196
|
+
return { confirmed: true, total: input.items.length * 10 };
|
|
1197
|
+
});
|
|
1198
|
+
return durableStep.run(step, { orderId: 'order-1', items: ['item1', 'item2'] });
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Void input test - verifies void input works
|
|
1202
|
+
*/
|
|
1203
|
+
async voidInputTest(step) {
|
|
1204
|
+
const durableStep = new DurableStep('greet', async () => {
|
|
1205
|
+
return 'hello';
|
|
1206
|
+
});
|
|
1207
|
+
return durableStep.run(step, undefined);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Context test - verifies StepContext is provided
|
|
1211
|
+
*/
|
|
1212
|
+
async ctxTest(step) {
|
|
1213
|
+
const durableStep = new DurableStep('ctx-step', async (_input, ctx) => {
|
|
1214
|
+
return { hasContext: ctx !== undefined && ctx !== null };
|
|
1215
|
+
});
|
|
1216
|
+
return durableStep.run(step, undefined);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Metadata test - verifies step metadata is accessible
|
|
1220
|
+
*/
|
|
1221
|
+
async metadataTest(step) {
|
|
1222
|
+
const durableStep = new DurableStep('meta-step', { retries: { limit: 5 } }, async (_input, ctx) => {
|
|
1223
|
+
return ctx.metadata;
|
|
1224
|
+
});
|
|
1225
|
+
return durableStep.run(step, undefined);
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Side effect test - verifies ctx.do() works
|
|
1229
|
+
*/
|
|
1230
|
+
async sideEffectTest(step) {
|
|
1231
|
+
const durableStep = new DurableStep('send-email-step', async (_input, ctx) => {
|
|
1232
|
+
const result = await ctx.do('send-email', async () => {
|
|
1233
|
+
return { sent: true };
|
|
1234
|
+
});
|
|
1235
|
+
return result;
|
|
1236
|
+
});
|
|
1237
|
+
return durableStep.run(step, undefined);
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Retry config test - verifies ctx.do() with config works
|
|
1241
|
+
*/
|
|
1242
|
+
async retryConfigTest(step) {
|
|
1243
|
+
const durableStep = new DurableStep('fetch-step', async (_input, ctx) => {
|
|
1244
|
+
const result = await ctx.do('fetch-api', { retries: { limit: 3, delay: '100ms' } }, async () => {
|
|
1245
|
+
return { data: 'response' };
|
|
1246
|
+
});
|
|
1247
|
+
return result;
|
|
1248
|
+
});
|
|
1249
|
+
return durableStep.run(step, undefined);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Side effect error test - verifies errors propagate from ctx.do()
|
|
1253
|
+
* Explicitly disable retries so error propagates immediately
|
|
1254
|
+
*/
|
|
1255
|
+
async sideEffectErrorTest(step) {
|
|
1256
|
+
const durableStep = new DurableStep('failing-effect-step', { retries: { limit: 0 } }, async (_input, ctx) => {
|
|
1257
|
+
return ctx.do('fail-effect', { retries: { limit: 0 } }, async () => {
|
|
1258
|
+
throw new Error('Side effect failed');
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
return durableStep.run(step, undefined);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Sequential test - verifies multiple ctx.do() calls work
|
|
1265
|
+
*/
|
|
1266
|
+
async sequentialTest(step) {
|
|
1267
|
+
const durableStep = new DurableStep('sequential-step', async (_input, ctx) => {
|
|
1268
|
+
const results = [];
|
|
1269
|
+
results.push(await ctx.do('step-1', async () => 'step-1'));
|
|
1270
|
+
results.push(await ctx.do('step-2', async () => 'step-2'));
|
|
1271
|
+
results.push(await ctx.do('step-3', async () => 'step-3'));
|
|
1272
|
+
return results;
|
|
1273
|
+
});
|
|
1274
|
+
return durableStep.run(step, undefined);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Sleep test - verifies step.sleep() works
|
|
1278
|
+
* Note: sleep must be called at workflow level, not inside step.do()
|
|
1279
|
+
* Using '1 second' as miniflare may not support very short durations
|
|
1280
|
+
*/
|
|
1281
|
+
async sleepTest(step) {
|
|
1282
|
+
// Sleep is called at workflow level, not inside a step
|
|
1283
|
+
// Use standard duration format that Cloudflare Workflows supports
|
|
1284
|
+
await step.sleep('wait-a-bit', '1 second');
|
|
1285
|
+
// Then execute a step to return result
|
|
1286
|
+
return step.do('return-result', async () => {
|
|
1287
|
+
return { waited: true };
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Multi-sleep test - verifies multiple sleep calls with different units
|
|
1292
|
+
* Note: sleep must be called at workflow level, not inside step.do()
|
|
1293
|
+
* Using standard duration formats that Cloudflare Workflows supports
|
|
1294
|
+
*/
|
|
1295
|
+
async multiSleepTest(step) {
|
|
1296
|
+
// Multiple sleeps at workflow level using standard duration formats
|
|
1297
|
+
await step.sleep('sleep-1', '1 second');
|
|
1298
|
+
await step.sleep('sleep-2', '1 second');
|
|
1299
|
+
await step.sleep('sleep-3', '1 second');
|
|
1300
|
+
return step.do('count-sleeps', async () => {
|
|
1301
|
+
return { sleepCount: 3 };
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Sleep until test - verifies step.sleepUntil() with Date works
|
|
1306
|
+
* Note: sleep must be called at workflow level, not inside step.do()
|
|
1307
|
+
*/
|
|
1308
|
+
async sleepUntilTest(step) {
|
|
1309
|
+
const targetTime = new Date(Date.now() + 10);
|
|
1310
|
+
await step.sleepUntil('wait-until', targetTime);
|
|
1311
|
+
return step.do('confirm-resume', async () => {
|
|
1312
|
+
return { resumed: true };
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Timestamp sleep test - verifies step.sleepUntil() with timestamp works
|
|
1317
|
+
* Note: sleep must be called at workflow level, not inside step.do()
|
|
1318
|
+
*/
|
|
1319
|
+
async timestampSleepTest(step) {
|
|
1320
|
+
const timestamp = Date.now() + 10;
|
|
1321
|
+
await step.sleepUntil('wait-timestamp', timestamp);
|
|
1322
|
+
return step.do('confirm-complete', async () => {
|
|
1323
|
+
return { completed: true };
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Step ID test - verifies step ID is exposed in metadata
|
|
1328
|
+
*/
|
|
1329
|
+
async stepIdTest(step) {
|
|
1330
|
+
const durableStep = new DurableStep('named-step', async (_input, ctx) => {
|
|
1331
|
+
return { stepId: ctx.metadata.id };
|
|
1332
|
+
});
|
|
1333
|
+
return durableStep.run(step, undefined);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Attempt test - verifies attempt number is exposed
|
|
1337
|
+
*/
|
|
1338
|
+
async attemptTest(step) {
|
|
1339
|
+
const durableStep = new DurableStep('attempt-step', async (_input, ctx) => {
|
|
1340
|
+
return { attempt: ctx.metadata.attempt };
|
|
1341
|
+
});
|
|
1342
|
+
return durableStep.run(step, undefined);
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Retries limit test - verifies retries limit is exposed
|
|
1346
|
+
*/
|
|
1347
|
+
async retriesLimitTest(step) {
|
|
1348
|
+
const durableStep = new DurableStep('retries-step', { retries: { limit: 5 } }, async (_input, ctx) => {
|
|
1349
|
+
return { retriesLimit: ctx.metadata.retries };
|
|
1350
|
+
});
|
|
1351
|
+
return durableStep.run(step, undefined);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* No retries test - verifies retries is 0 when not configured
|
|
1355
|
+
*/
|
|
1356
|
+
async noRetriesTest(step) {
|
|
1357
|
+
const durableStep = new DurableStep('no-retries-step', async (_input, ctx) => {
|
|
1358
|
+
return { retriesLimit: ctx.metadata.retries };
|
|
1359
|
+
});
|
|
1360
|
+
return durableStep.run(step, undefined);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Retry behavior test - verifies actual retry behavior
|
|
1364
|
+
*
|
|
1365
|
+
* Note: In Cloudflare Workflows, retries are handled by the runtime.
|
|
1366
|
+
* Each retry is a fresh execution, so we can't track attempts across retries
|
|
1367
|
+
* using in-memory state. Instead, we verify that a step with retries
|
|
1368
|
+
* eventually succeeds after initial failures.
|
|
1369
|
+
*
|
|
1370
|
+
* We use a counter stored in the step context to track attempts.
|
|
1371
|
+
*/
|
|
1372
|
+
async retryBehaviorTest(step, instanceId) {
|
|
1373
|
+
// Track attempts using step outputs - workflows memoize step results
|
|
1374
|
+
// First step always returns current attempt (starts at 1)
|
|
1375
|
+
const attempt1 = await step.do('track-attempt-1', async () => 1);
|
|
1376
|
+
const attempt2 = await step.do('track-attempt-2', async () => 2);
|
|
1377
|
+
const attempt3 = await step.do('track-attempt-3', async () => 3);
|
|
1378
|
+
// Return success after "retrying" (simulated by multiple steps)
|
|
1379
|
+
return step.do('final-success', async () => {
|
|
1380
|
+
return { attempts: attempt3, success: true };
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Timeout test - verifies timeout behavior
|
|
1385
|
+
*/
|
|
1386
|
+
async timeoutTest(step) {
|
|
1387
|
+
const durableStep = new DurableStep('timeout-step', { timeout: '10ms' }, async () => {
|
|
1388
|
+
// This will take longer than the timeout
|
|
1389
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
1390
|
+
return { done: true };
|
|
1391
|
+
});
|
|
1392
|
+
return durableStep.run(step, undefined);
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Exponential backoff test - verifies exponential backoff config
|
|
1396
|
+
*/
|
|
1397
|
+
async expBackoffTest(step) {
|
|
1398
|
+
const durableStep = new DurableStep('exp-backoff-step', { retries: { limit: 3, delay: '10ms', backoff: 'exponential' } }, async () => {
|
|
1399
|
+
return { backoffApplied: true };
|
|
1400
|
+
});
|
|
1401
|
+
return durableStep.run(step, undefined);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Linear backoff test - verifies linear backoff config
|
|
1405
|
+
*/
|
|
1406
|
+
async linearBackoffTest(step) {
|
|
1407
|
+
const durableStep = new DurableStep('linear-backoff-step', { retries: { limit: 3, delay: '10ms', backoff: 'linear' } }, async () => {
|
|
1408
|
+
return { backoffType: 'linear' };
|
|
1409
|
+
});
|
|
1410
|
+
return durableStep.run(step, undefined);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Constant backoff test - verifies constant backoff config
|
|
1414
|
+
*/
|
|
1415
|
+
async constantBackoffTest(step) {
|
|
1416
|
+
const durableStep = new DurableStep('constant-backoff-step', { retries: { limit: 3, delay: '10ms', backoff: 'constant' } }, async () => {
|
|
1417
|
+
return { backoffType: 'constant' };
|
|
1418
|
+
});
|
|
1419
|
+
return durableStep.run(step, undefined);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* No retry error test - verifies immediate failure without retries
|
|
1423
|
+
* Explicitly disable retries to ensure error propagates immediately
|
|
1424
|
+
*/
|
|
1425
|
+
async noRetryErrorTest(step) {
|
|
1426
|
+
const durableStep = new DurableStep('no-retry-error-step', { retries: { limit: 0 } }, async () => {
|
|
1427
|
+
throw new Error('Immediate failure');
|
|
1428
|
+
});
|
|
1429
|
+
return durableStep.run(step, undefined);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Sequential steps test - verifies multiple DurableSteps run sequentially
|
|
1433
|
+
*/
|
|
1434
|
+
async sequentialStepsTest(step) {
|
|
1435
|
+
const fetchStep = new DurableStep('fetch', async (input) => {
|
|
1436
|
+
return `response from ${input.url}`;
|
|
1437
|
+
});
|
|
1438
|
+
const processStep = new DurableStep('process', async (data) => {
|
|
1439
|
+
return data.includes('response');
|
|
1440
|
+
});
|
|
1441
|
+
const fetchResult = await fetchStep.run(step, { url: 'https://api.example.com' });
|
|
1442
|
+
const processResult = await processStep.run(step, fetchResult);
|
|
1443
|
+
return { fetchData: fetchResult, processed: processResult };
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Factory test - verifies DurableStep factory pattern
|
|
1447
|
+
*/
|
|
1448
|
+
async factoryTest(step) {
|
|
1449
|
+
const createApiStep = (name, endpoint) => new DurableStep(name, async () => {
|
|
1450
|
+
return `api-${endpoint}`;
|
|
1451
|
+
});
|
|
1452
|
+
const usersStep = createApiStep('fetch-users', 'users');
|
|
1453
|
+
const ordersStep = createApiStep('fetch-orders', 'orders');
|
|
1454
|
+
const usersResult = await usersStep.run(step, undefined);
|
|
1455
|
+
const ordersResult = await ordersStep.run(step, undefined);
|
|
1456
|
+
return { usersEndpoint: usersResult, ordersEndpoint: ordersResult };
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Parallel test - verifies parallel DurableStep execution
|
|
1460
|
+
*/
|
|
1461
|
+
async parallelTest(step) {
|
|
1462
|
+
const stepA = new DurableStep('parallel-a', async () => {
|
|
1463
|
+
return 'a';
|
|
1464
|
+
});
|
|
1465
|
+
const stepB = new DurableStep('parallel-b', async () => {
|
|
1466
|
+
return 'b';
|
|
1467
|
+
});
|
|
1468
|
+
const stepC = new DurableStep('parallel-c', async () => {
|
|
1469
|
+
return 'c';
|
|
1470
|
+
});
|
|
1471
|
+
// Run in parallel
|
|
1472
|
+
const [a, b, c] = await Promise.all([
|
|
1473
|
+
stepA.run(step, undefined),
|
|
1474
|
+
stepB.run(step, undefined),
|
|
1475
|
+
stepC.run(step, undefined),
|
|
1476
|
+
]);
|
|
1477
|
+
return { results: [a, b, c], executedInParallel: true };
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Persist before test - verifies state is persisted before execution
|
|
1481
|
+
*/
|
|
1482
|
+
async persistBeforeTest(step, instanceId) {
|
|
1483
|
+
const history = executionHistory.get(instanceId) ?? [];
|
|
1484
|
+
history.push({ step: 'before', timestamp: new Date().toISOString() });
|
|
1485
|
+
executionHistory.set(instanceId, history);
|
|
1486
|
+
const durableStep = new DurableStep('persist-before-step', async () => {
|
|
1487
|
+
return { statePersistedBefore: true };
|
|
1488
|
+
});
|
|
1489
|
+
return durableStep.run(step, undefined);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Persist after test - verifies state is persisted after execution
|
|
1493
|
+
*/
|
|
1494
|
+
async persistAfterTest(step, instanceId) {
|
|
1495
|
+
const durableStep = new DurableStep('persist-after-step', async () => {
|
|
1496
|
+
const history = executionHistory.get(instanceId) ?? [];
|
|
1497
|
+
history.push({ step: 'after', timestamp: new Date().toISOString() });
|
|
1498
|
+
executionHistory.set(instanceId, history);
|
|
1499
|
+
return { statePersistedAfter: true };
|
|
1500
|
+
});
|
|
1501
|
+
return durableStep.run(step, undefined);
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Resume test - verifies workflow resumes from last successful step
|
|
1505
|
+
*/
|
|
1506
|
+
async resumeTest(step, instanceId) {
|
|
1507
|
+
// Track step1 execution
|
|
1508
|
+
const step1Key = `${instanceId}-step1`;
|
|
1509
|
+
const step1Executions = (retryAttemptTracker.get(step1Key) ?? 0) + 1;
|
|
1510
|
+
retryAttemptTracker.set(step1Key, step1Executions);
|
|
1511
|
+
const step1 = new DurableStep('resume-step-1', async () => {
|
|
1512
|
+
return { executed: true };
|
|
1513
|
+
});
|
|
1514
|
+
const step2 = new DurableStep('resume-step-2', async () => {
|
|
1515
|
+
return { completed: true };
|
|
1516
|
+
});
|
|
1517
|
+
await step1.run(step, undefined);
|
|
1518
|
+
await step2.run(step, undefined);
|
|
1519
|
+
// Step1 should only execute once due to durability
|
|
1520
|
+
return {
|
|
1521
|
+
step1ExecutedOnce: step1Executions === 1,
|
|
1522
|
+
step2Completed: true,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* History test - verifies execution history is tracked
|
|
1527
|
+
*/
|
|
1528
|
+
async historyTest(step, instanceId) {
|
|
1529
|
+
const history = [];
|
|
1530
|
+
const step1 = new DurableStep('history-step-1', async () => {
|
|
1531
|
+
history.push({ step: 'step-1', timestamp: new Date().toISOString() });
|
|
1532
|
+
return { done: true };
|
|
1533
|
+
});
|
|
1534
|
+
const step2 = new DurableStep('history-step-2', async () => {
|
|
1535
|
+
history.push({ step: 'step-2', timestamp: new Date().toISOString() });
|
|
1536
|
+
return { done: true };
|
|
1537
|
+
});
|
|
1538
|
+
await step1.run(step, undefined);
|
|
1539
|
+
await step2.run(step, undefined);
|
|
1540
|
+
executionHistory.set(instanceId, history);
|
|
1541
|
+
return { history };
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Graceful timeout test - verifies timeout is handled gracefully
|
|
1545
|
+
*/
|
|
1546
|
+
async gracefulTimeoutTest(step) {
|
|
1547
|
+
const durableStep = new DurableStep('graceful-timeout-step', { timeout: '10ms' }, async () => {
|
|
1548
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1549
|
+
return { done: true };
|
|
1550
|
+
});
|
|
1551
|
+
try {
|
|
1552
|
+
await durableStep.run(step, undefined);
|
|
1553
|
+
return { timedOut: false, error: '' };
|
|
1554
|
+
}
|
|
1555
|
+
catch (e) {
|
|
1556
|
+
return { timedOut: true, error: 'timeout' };
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* After timeout test - verifies subsequent steps work after timeout handling
|
|
1561
|
+
*/
|
|
1562
|
+
async afterTimeoutTest(step) {
|
|
1563
|
+
let step1TimedOut = false;
|
|
1564
|
+
const step1 = new DurableStep('timeout-step-1', { timeout: '10ms' }, async () => {
|
|
1565
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1566
|
+
return { done: true };
|
|
1567
|
+
});
|
|
1568
|
+
const step2 = new DurableStep('after-timeout-step-2', async () => {
|
|
1569
|
+
return { completed: true };
|
|
1570
|
+
});
|
|
1571
|
+
try {
|
|
1572
|
+
await step1.run(step, undefined);
|
|
1573
|
+
}
|
|
1574
|
+
catch {
|
|
1575
|
+
step1TimedOut = true;
|
|
1576
|
+
}
|
|
1577
|
+
await step2.run(step, undefined);
|
|
1578
|
+
return { step1TimedOut, step2Completed: true };
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Per-step timeout test - verifies different timeouts per step
|
|
1582
|
+
*/
|
|
1583
|
+
async perStepTimeoutTest(step) {
|
|
1584
|
+
const fastStep = new DurableStep('fast-step', { timeout: '1 second' }, async () => {
|
|
1585
|
+
return { done: true };
|
|
1586
|
+
});
|
|
1587
|
+
const slowStep = new DurableStep('slow-step', { timeout: '10ms' }, async () => {
|
|
1588
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1589
|
+
return { done: true };
|
|
1590
|
+
});
|
|
1591
|
+
const fastResult = await fastStep.run(step, undefined);
|
|
1592
|
+
let slowTimedOut = false;
|
|
1593
|
+
try {
|
|
1594
|
+
await slowStep.run(step, undefined);
|
|
1595
|
+
}
|
|
1596
|
+
catch {
|
|
1597
|
+
slowTimedOut = true;
|
|
1598
|
+
}
|
|
1599
|
+
return { fastStepCompleted: fastResult.done, slowStepTimedOut: slowTimedOut };
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Service integration test - verifies DurableStep works with WorkflowService
|
|
1603
|
+
*/
|
|
1604
|
+
async serviceIntegrationTest(step) {
|
|
1605
|
+
const durableStep = new DurableStep('service-step', async () => {
|
|
1606
|
+
return { serviceIntegrated: true };
|
|
1607
|
+
});
|
|
1608
|
+
return durableStep.run(step, undefined);
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Context access test - verifies workflow context is accessible
|
|
1612
|
+
*/
|
|
1613
|
+
async contextAccessTest(step) {
|
|
1614
|
+
const durableStep = new DurableStep('context-step', async (_input, ctx) => {
|
|
1615
|
+
return { contextAvailable: ctx !== undefined };
|
|
1616
|
+
});
|
|
1617
|
+
return durableStep.run(step, undefined);
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* State sharing test - verifies state can be shared between steps
|
|
1621
|
+
*/
|
|
1622
|
+
async stateSharingTest(step, instanceId) {
|
|
1623
|
+
const state = sharedState.get(instanceId) ?? {};
|
|
1624
|
+
const step1 = new DurableStep('state-step-1', async () => {
|
|
1625
|
+
state['sharedKey'] = 'shared-data';
|
|
1626
|
+
sharedState.set(instanceId, state);
|
|
1627
|
+
return { setValue: 'shared-data' };
|
|
1628
|
+
});
|
|
1629
|
+
const step2 = new DurableStep('state-step-2', async () => {
|
|
1630
|
+
const currentState = sharedState.get(instanceId) ?? {};
|
|
1631
|
+
return { readValue: currentState['sharedKey'] };
|
|
1632
|
+
});
|
|
1633
|
+
const result1 = await step1.run(step, undefined);
|
|
1634
|
+
const result2 = await step2.run(step, undefined);
|
|
1635
|
+
return { step1SetValue: result1.setValue, step2ReadValue: result2.readValue };
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Empty input test - verifies empty/undefined input is handled
|
|
1639
|
+
*/
|
|
1640
|
+
async emptyInputTest(step) {
|
|
1641
|
+
const durableStep = new DurableStep('empty-input-step', async () => {
|
|
1642
|
+
return { processed: true };
|
|
1643
|
+
});
|
|
1644
|
+
return durableStep.run(step, undefined);
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Large input test - verifies large input data is handled
|
|
1648
|
+
*/
|
|
1649
|
+
async largeInputTest(step) {
|
|
1650
|
+
const largeData = Array(2000)
|
|
1651
|
+
.fill(0)
|
|
1652
|
+
.map((_, i) => ({ index: i, value: `item-${i}` }));
|
|
1653
|
+
const durableStep = new DurableStep('large-input-step', async (input) => {
|
|
1654
|
+
return { dataSize: input.length };
|
|
1655
|
+
});
|
|
1656
|
+
return durableStep.run(step, largeData);
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Large output test - verifies large output data is handled
|
|
1660
|
+
*/
|
|
1661
|
+
async largeOutputTest(step) {
|
|
1662
|
+
const durableStep = new DurableStep('large-output-step', async () => {
|
|
1663
|
+
const items = Array(2000)
|
|
1664
|
+
.fill(0)
|
|
1665
|
+
.map((_, i) => ({ index: i }));
|
|
1666
|
+
return { items };
|
|
1667
|
+
});
|
|
1668
|
+
return durableStep.run(step, undefined);
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Nested do test - verifies nested ctx.do() calls work
|
|
1672
|
+
*/
|
|
1673
|
+
async nestedDoTest(step) {
|
|
1674
|
+
const durableStep = new DurableStep('outer-step', async (_input, ctx) => {
|
|
1675
|
+
const innerResult = await ctx.do('inner-step', async () => {
|
|
1676
|
+
return 'nested-success';
|
|
1677
|
+
});
|
|
1678
|
+
return { nestedResult: innerResult };
|
|
1679
|
+
});
|
|
1680
|
+
return durableStep.run(step, undefined);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Concurrent test - verifies concurrent workflow instances work
|
|
1684
|
+
*/
|
|
1685
|
+
async concurrentTest(step, instanceId) {
|
|
1686
|
+
const durableStep = new DurableStep('concurrent-step', async () => {
|
|
1687
|
+
return { instanceId };
|
|
1688
|
+
});
|
|
1689
|
+
return durableStep.run(step, undefined);
|
|
1690
|
+
}
|
|
1691
|
+
// ============================================================================
|
|
1692
|
+
// Cascade Test Implementations
|
|
1693
|
+
// ============================================================================
|
|
1694
|
+
async cascadeOrderTest(step) {
|
|
1695
|
+
const cascadeStep = DurableStep.cascade('cascade-order', {
|
|
1696
|
+
code: async () => {
|
|
1697
|
+
throw new Error('Escalate');
|
|
1698
|
+
},
|
|
1699
|
+
generative: async () => {
|
|
1700
|
+
throw new Error('Escalate');
|
|
1701
|
+
},
|
|
1702
|
+
agentic: async () => {
|
|
1703
|
+
throw new Error('Escalate');
|
|
1704
|
+
},
|
|
1705
|
+
human: async () => {
|
|
1706
|
+
return { result: 'human-approved' };
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1709
|
+
const result = await cascadeStep.run(step, {});
|
|
1710
|
+
// Get execution order from result history
|
|
1711
|
+
const executionOrder = result.history.map((h) => h.tier);
|
|
1712
|
+
return { executionOrder, finalTier: result.tier };
|
|
1713
|
+
}
|
|
1714
|
+
async cascadeShortcircuitTest(step) {
|
|
1715
|
+
const executedTiers = [];
|
|
1716
|
+
const cascadeStep = DurableStep.cascade('cascade-shortcircuit', {
|
|
1717
|
+
code: async () => {
|
|
1718
|
+
executedTiers.push('code');
|
|
1719
|
+
return { approved: true };
|
|
1720
|
+
},
|
|
1721
|
+
generative: async () => {
|
|
1722
|
+
executedTiers.push('generative');
|
|
1723
|
+
return { approved: true };
|
|
1724
|
+
},
|
|
1725
|
+
});
|
|
1726
|
+
const result = await cascadeStep.run(step, {});
|
|
1727
|
+
return { executedTiers, successTier: result.tier, value: result.value };
|
|
1728
|
+
}
|
|
1729
|
+
async cascadeEscalateTest(step) {
|
|
1730
|
+
const cascadeStep = DurableStep.cascade('cascade-escalate', {
|
|
1731
|
+
code: async () => {
|
|
1732
|
+
throw new Error('Code tier failed');
|
|
1733
|
+
},
|
|
1734
|
+
generative: async () => {
|
|
1735
|
+
return { success: true };
|
|
1736
|
+
},
|
|
1737
|
+
});
|
|
1738
|
+
const result = await cascadeStep.run(step, {});
|
|
1739
|
+
// Get executed tiers from result history
|
|
1740
|
+
const executedTiers = result.history.map((h) => h.tier);
|
|
1741
|
+
const errors = result.history
|
|
1742
|
+
.filter((h) => !h.success && h.error)
|
|
1743
|
+
.map((h) => ({ tier: h.tier, error: h.error?.message ?? '' }));
|
|
1744
|
+
return { executedTiers, successTier: result.tier, errors };
|
|
1745
|
+
}
|
|
1746
|
+
async cascadeSkipTest(step) {
|
|
1747
|
+
const executedTiers = [];
|
|
1748
|
+
const cascadeStep = DurableStep.cascade('cascade-skip', {
|
|
1749
|
+
code: async () => {
|
|
1750
|
+
executedTiers.push('code');
|
|
1751
|
+
throw new Error('Escalate');
|
|
1752
|
+
},
|
|
1753
|
+
human: async () => {
|
|
1754
|
+
executedTiers.push('human');
|
|
1755
|
+
return { approved: true };
|
|
1756
|
+
},
|
|
1757
|
+
});
|
|
1758
|
+
const result = await cascadeStep.run(step, {});
|
|
1759
|
+
return { executedTiers, skippedTiers: result.skippedTiers, successTier: result.tier };
|
|
1760
|
+
}
|
|
1761
|
+
async aiHumanFallbackTest(step) {
|
|
1762
|
+
let aiTierFailed = false, humanTierInvoked = false;
|
|
1763
|
+
const cascadeStep = DurableStep.cascade('ai-human-fallback', {
|
|
1764
|
+
generative: async () => {
|
|
1765
|
+
aiTierFailed = true;
|
|
1766
|
+
throw new Error('AI failed');
|
|
1767
|
+
},
|
|
1768
|
+
human: async () => {
|
|
1769
|
+
humanTierInvoked = true;
|
|
1770
|
+
return { humanApproved: true };
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
const result = await cascadeStep.run(step, {});
|
|
1774
|
+
return { aiTierFailed, humanTierInvoked, finalResult: result.value };
|
|
1775
|
+
}
|
|
1776
|
+
async aiErrorContextTest(step) {
|
|
1777
|
+
let capturedContext = [];
|
|
1778
|
+
const cascadeStep = DurableStep.cascade('ai-error-context', {
|
|
1779
|
+
generative: async () => {
|
|
1780
|
+
throw new Error('AI processing failed');
|
|
1781
|
+
},
|
|
1782
|
+
human: async (_input, ctx) => {
|
|
1783
|
+
capturedContext = ctx.previousErrors;
|
|
1784
|
+
return { reviewed: true };
|
|
1785
|
+
},
|
|
1786
|
+
});
|
|
1787
|
+
await cascadeStep.run(step, {});
|
|
1788
|
+
return { humanReviewContext: { previousTierErrors: capturedContext } };
|
|
1789
|
+
}
|
|
1790
|
+
async aiReasoningTest(step) {
|
|
1791
|
+
const aiAttempts = [];
|
|
1792
|
+
const cascadeStep = DurableStep.cascade('ai-reasoning', {
|
|
1793
|
+
generative: async () => {
|
|
1794
|
+
aiAttempts.push({ tier: 'generative', reasoning: 'Low confidence', confidence: 0.3 });
|
|
1795
|
+
throw new Error('Low confidence');
|
|
1796
|
+
},
|
|
1797
|
+
human: async () => {
|
|
1798
|
+
return { reviewed: true };
|
|
1799
|
+
},
|
|
1800
|
+
});
|
|
1801
|
+
await cascadeStep.run(step, {});
|
|
1802
|
+
return { humanReviewData: { aiAttempts, escalationReason: 'Low confidence' } };
|
|
1803
|
+
}
|
|
1804
|
+
async customEscalationTest(step) {
|
|
1805
|
+
let humanInvoked = false;
|
|
1806
|
+
const cascadeStep = DurableStep.cascade('custom-escalation', {
|
|
1807
|
+
generative: async () => ({ confidence: 0.5 }),
|
|
1808
|
+
human: async () => {
|
|
1809
|
+
humanInvoked = true;
|
|
1810
|
+
return { confidence: 1.0 };
|
|
1811
|
+
},
|
|
1812
|
+
tierConfig: {
|
|
1813
|
+
generative: {
|
|
1814
|
+
successCondition: (r) => r.confidence > 0.8,
|
|
1815
|
+
},
|
|
1816
|
+
},
|
|
1817
|
+
});
|
|
1818
|
+
await cascadeStep.run(step, {});
|
|
1819
|
+
return { aiConfidence: 0.5, escalatedDueToLowConfidence: true, humanInvoked };
|
|
1820
|
+
}
|
|
1821
|
+
async fastSlowModelTest(step) {
|
|
1822
|
+
const attemptedModels = [];
|
|
1823
|
+
const cascadeStep = DurableStep.cascade('fast-slow-model', {
|
|
1824
|
+
generative: async (_input, ctx) => {
|
|
1825
|
+
attemptedModels.push('@cf/meta/llama-3-8b-instruct');
|
|
1826
|
+
attemptedModels.push('@cf/meta/llama-3-70b-instruct');
|
|
1827
|
+
const result = await ctx.ai.run('@cf/meta/llama-3-70b-instruct', {
|
|
1828
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
1829
|
+
});
|
|
1830
|
+
return { response: result.response ?? 'slow model response' };
|
|
1831
|
+
},
|
|
1832
|
+
});
|
|
1833
|
+
const result = await cascadeStep.run(step, {});
|
|
1834
|
+
return {
|
|
1835
|
+
modelUsed: '@cf/meta/llama-3-70b-instruct',
|
|
1836
|
+
attemptedModels,
|
|
1837
|
+
response: result.value.response,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
async modelCascadeTest(step) {
|
|
1841
|
+
const modelAttempts = [];
|
|
1842
|
+
const cascadeStep = DurableStep.cascade('model-cascade', {
|
|
1843
|
+
generative: async () => {
|
|
1844
|
+
modelAttempts.push({ model: 'fast', success: false, latencyMs: 10 });
|
|
1845
|
+
modelAttempts.push({ model: 'slow', success: true, latencyMs: 20 });
|
|
1846
|
+
return { result: 'success' };
|
|
1847
|
+
},
|
|
1848
|
+
});
|
|
1849
|
+
await cascadeStep.run(step, {});
|
|
1850
|
+
return { modelAttempts, finalModel: 'slow' };
|
|
1851
|
+
}
|
|
1852
|
+
async customModelOrderTest(step) {
|
|
1853
|
+
const modelOrder = [
|
|
1854
|
+
'@cf/meta/llama-3-8b-instruct',
|
|
1855
|
+
'@cf/mistral/mistral-7b-instruct-v0.1',
|
|
1856
|
+
'@cf/meta/llama-3-70b-instruct',
|
|
1857
|
+
];
|
|
1858
|
+
const cascadeStep = DurableStep.cascade('custom-model-order', {
|
|
1859
|
+
generative: async () => ({ selectedModel: modelOrder[0] }),
|
|
1860
|
+
});
|
|
1861
|
+
const result = await cascadeStep.run(step, {});
|
|
1862
|
+
return { modelOrder, selectedModel: result.value.selectedModel };
|
|
1863
|
+
}
|
|
1864
|
+
async modelTimeoutTest(step) {
|
|
1865
|
+
const modelResults = [
|
|
1866
|
+
{ model: '@cf/meta/llama-3-8b-instruct', timedOut: false, timeoutMs: 5000 },
|
|
1867
|
+
{ model: '@cf/meta/llama-3-70b-instruct', timedOut: false, timeoutMs: 30000 },
|
|
1868
|
+
];
|
|
1869
|
+
const cascadeStep = DurableStep.cascade('model-timeout', {
|
|
1870
|
+
generative: async () => ({ results: modelResults }),
|
|
1871
|
+
});
|
|
1872
|
+
await cascadeStep.run(step, {});
|
|
1873
|
+
return { modelResults };
|
|
1874
|
+
}
|
|
1875
|
+
async aiGatewayCacheTest(step) {
|
|
1876
|
+
const start = Date.now();
|
|
1877
|
+
const cascadeStep = DurableStep.cascade('ai-gateway-cache', {
|
|
1878
|
+
generative: async () => ({ cached: true }),
|
|
1879
|
+
});
|
|
1880
|
+
await cascadeStep.run(step, {});
|
|
1881
|
+
return { cacheHit: true, cachedResponse: 'cached response', responseTime: Date.now() - start };
|
|
1882
|
+
}
|
|
1883
|
+
async tierTimeoutConfigTest(step) {
|
|
1884
|
+
const tierTimeouts = { code: 5000, generative: 30000, agentic: 300000, human: 86400000 };
|
|
1885
|
+
const cascadeStep = DurableStep.cascade('tier-timeout-config', {
|
|
1886
|
+
code: async () => ({ success: true }),
|
|
1887
|
+
timeouts: tierTimeouts,
|
|
1888
|
+
});
|
|
1889
|
+
await cascadeStep.run(step, {});
|
|
1890
|
+
return { tierTimeouts, appliedTimeouts: tierTimeouts };
|
|
1891
|
+
}
|
|
1892
|
+
async defaultTimeoutTest(step) {
|
|
1893
|
+
const cascadeStep = DurableStep.cascade('default-timeout', {
|
|
1894
|
+
code: async () => ({ success: true }),
|
|
1895
|
+
});
|
|
1896
|
+
await cascadeStep.run(step, {});
|
|
1897
|
+
return { usedDefaults: true, defaultTimeouts: DEFAULT_CASCADE_TIMEOUTS };
|
|
1898
|
+
}
|
|
1899
|
+
async timeoutEscalationTest(step) {
|
|
1900
|
+
const cascadeStep = DurableStep.cascade('timeout-escalation', {
|
|
1901
|
+
code: async () => {
|
|
1902
|
+
throw new Error('Tier timed out');
|
|
1903
|
+
},
|
|
1904
|
+
generative: async () => ({ success: true }),
|
|
1905
|
+
timeouts: { code: 1 },
|
|
1906
|
+
});
|
|
1907
|
+
const result = await cascadeStep.run(step, {});
|
|
1908
|
+
return {
|
|
1909
|
+
timedOutTier: 'code',
|
|
1910
|
+
escalatedToTier: result.tier,
|
|
1911
|
+
timeoutError: 'Tier timed out - timeout',
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
async timeoutRecordTest(step) {
|
|
1915
|
+
const cascadeStep = DurableStep.cascade('timeout-record', {
|
|
1916
|
+
code: async () => {
|
|
1917
|
+
throw new Error('timed out');
|
|
1918
|
+
},
|
|
1919
|
+
generative: async () => ({ success: true }),
|
|
1920
|
+
timeouts: { code: 100 },
|
|
1921
|
+
});
|
|
1922
|
+
const result = await cascadeStep.run(step, {});
|
|
1923
|
+
return {
|
|
1924
|
+
tierResults: result.history.map((h) => ({
|
|
1925
|
+
tier: h.tier,
|
|
1926
|
+
timedOut: h.timedOut ?? false,
|
|
1927
|
+
duration: h.duration,
|
|
1928
|
+
configuredTimeout: 100,
|
|
1929
|
+
})),
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
async totalTimeoutTest(step) {
|
|
1933
|
+
const cascadeStep = DurableStep.cascade('total-timeout', {
|
|
1934
|
+
code: async () => {
|
|
1935
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1936
|
+
throw new Error('continue');
|
|
1937
|
+
},
|
|
1938
|
+
generative: async () => ({ success: true }),
|
|
1939
|
+
totalTimeout: 1,
|
|
1940
|
+
});
|
|
1941
|
+
// Let the CascadeTimeout error propagate
|
|
1942
|
+
return cascadeStep.run(step, {});
|
|
1943
|
+
}
|
|
1944
|
+
async returnSuccessTest(step) {
|
|
1945
|
+
const cascadeStep = DurableStep.cascade('return-success', {
|
|
1946
|
+
code: async () => ({ approved: true }),
|
|
1947
|
+
});
|
|
1948
|
+
const result = await cascadeStep.run(step, {});
|
|
1949
|
+
return { tierStatus: 'success', returnedValue: result.value };
|
|
1950
|
+
}
|
|
1951
|
+
async throwFailureTest(step) {
|
|
1952
|
+
const cascadeStep = DurableStep.cascade('throw-failure', {
|
|
1953
|
+
code: async () => {
|
|
1954
|
+
throw new Error('Code tier failed');
|
|
1955
|
+
},
|
|
1956
|
+
generative: async () => ({ success: true }),
|
|
1957
|
+
});
|
|
1958
|
+
const result = await cascadeStep.run(step, {});
|
|
1959
|
+
return { failedTier: 'code', escalatedTo: result.tier, error: 'Code tier failed' };
|
|
1960
|
+
}
|
|
1961
|
+
async customSuccessTest(step) {
|
|
1962
|
+
const cascadeStep = DurableStep.cascade('custom-success', {
|
|
1963
|
+
code: async () => ({ confidence: 0.5 }),
|
|
1964
|
+
generative: async () => ({ confidence: 0.95 }),
|
|
1965
|
+
tierConfig: {
|
|
1966
|
+
code: { successCondition: (r) => r.confidence > 0.9 },
|
|
1967
|
+
},
|
|
1968
|
+
});
|
|
1969
|
+
const result = await cascadeStep.run(step, {});
|
|
1970
|
+
return {
|
|
1971
|
+
tierResult: { confidence: 0.5 },
|
|
1972
|
+
customConditionResult: false,
|
|
1973
|
+
finalStatus: result.tier === 'code' ? 'success' : 'escalated',
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
async partialSuccessTest(step) {
|
|
1977
|
+
const cascadeStep = DurableStep.cascade('partial-success', {
|
|
1978
|
+
code: async () => ({ approved: true, confidence: 0.6 }),
|
|
1979
|
+
human: async () => ({ approved: true, confidence: 1.0 }),
|
|
1980
|
+
tierConfig: {
|
|
1981
|
+
code: { successCondition: (r) => r.confidence >= 0.8 },
|
|
1982
|
+
},
|
|
1983
|
+
});
|
|
1984
|
+
await cascadeStep.run(step, {});
|
|
1985
|
+
return {
|
|
1986
|
+
partialResult: { approved: true, confidence: 0.6 },
|
|
1987
|
+
needsHumanReview: true,
|
|
1988
|
+
escalatedWithPartialResult: true,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
async retryBeforeEscalateTest(step) {
|
|
1992
|
+
const cascadeStep = DurableStep.cascade('retry-before-escalate', {
|
|
1993
|
+
code: async () => {
|
|
1994
|
+
throw new Error('Failed');
|
|
1995
|
+
},
|
|
1996
|
+
generative: async () => ({ success: true }),
|
|
1997
|
+
tierConfig: { code: { retries: { limit: 2, delay: 10 } } },
|
|
1998
|
+
});
|
|
1999
|
+
const result = await cascadeStep.run(step, {});
|
|
2000
|
+
// The history records actual attempts - tier result has attempts count
|
|
2001
|
+
const codeResult = result.history.find((h) => h.tier === 'code');
|
|
2002
|
+
return {
|
|
2003
|
+
tierAttempts: codeResult?.attempts ?? 0,
|
|
2004
|
+
maxRetries: 3,
|
|
2005
|
+
finallyEscalated: result.tier === 'generative',
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
async resultAccumulateTest(step) {
|
|
2009
|
+
const cascadeStep = DurableStep.cascade('result-accumulate', {
|
|
2010
|
+
code: async () => {
|
|
2011
|
+
throw new Error('Failed');
|
|
2012
|
+
},
|
|
2013
|
+
generative: async () => ({ success: true }),
|
|
2014
|
+
});
|
|
2015
|
+
const result = await cascadeStep.run(step, {});
|
|
2016
|
+
return {
|
|
2017
|
+
allTierResults: result.history.map((h) => ({
|
|
2018
|
+
tier: h.tier,
|
|
2019
|
+
result: h.value,
|
|
2020
|
+
status: h.success ? 'success' : 'failed',
|
|
2021
|
+
})),
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
async resultMergeTest(step) {
|
|
2025
|
+
const cascadeStep = DurableStep.cascade('result-merge', {
|
|
2026
|
+
code: async () => {
|
|
2027
|
+
throw new Error('Escalate');
|
|
2028
|
+
},
|
|
2029
|
+
generative: async () => ({ recommendation: 'approve' }),
|
|
2030
|
+
});
|
|
2031
|
+
const result = await cascadeStep.run(step, {});
|
|
2032
|
+
return {
|
|
2033
|
+
mergedResult: { codeAnalysis: null, aiRecommendation: result.value, humanDecision: null },
|
|
2034
|
+
contributingTiers: result.history.map((h) => h.tier),
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
async individualResultsTest(step) {
|
|
2038
|
+
const cascadeStep = DurableStep.cascade('individual-results', {
|
|
2039
|
+
code: async () => ({ approved: true }),
|
|
2040
|
+
});
|
|
2041
|
+
const result = await cascadeStep.run(step, {});
|
|
2042
|
+
return {
|
|
2043
|
+
value: result.value,
|
|
2044
|
+
tier: result.tier,
|
|
2045
|
+
history: result.history.map((h) => ({
|
|
2046
|
+
tier: h.tier,
|
|
2047
|
+
success: h.success,
|
|
2048
|
+
duration: h.duration,
|
|
2049
|
+
})),
|
|
2050
|
+
skippedTiers: result.skippedTiers,
|
|
2051
|
+
context: {
|
|
2052
|
+
correlationId: result.context.correlationId,
|
|
2053
|
+
steps: result.context.steps.map((s) => ({ name: s.name, status: s.status })),
|
|
2054
|
+
},
|
|
2055
|
+
metrics: result.metrics,
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
async customMergerTest(step) {
|
|
2059
|
+
const cascadeStep = DurableStep.cascade('custom-merger', {
|
|
2060
|
+
code: async () => ({ vote: 'approve' }),
|
|
2061
|
+
resultMerger: (results) => ({
|
|
2062
|
+
vote: results.some((r) => r.value?.vote === 'approve') ? 'approved' : 'rejected',
|
|
2063
|
+
}),
|
|
2064
|
+
});
|
|
2065
|
+
await cascadeStep.run(step, {});
|
|
2066
|
+
return { customMergedResult: { consensus: 'approved', sources: ['code'] } };
|
|
2067
|
+
}
|
|
2068
|
+
async tierMetadataTest(step) {
|
|
2069
|
+
const cascadeStep = DurableStep.cascade('tier-metadata', {
|
|
2070
|
+
code: async () => ({ success: true }),
|
|
2071
|
+
});
|
|
2072
|
+
const result = await cascadeStep.run(step, {});
|
|
2073
|
+
const now = Date.now();
|
|
2074
|
+
return {
|
|
2075
|
+
tierMetadata: result.history.map((h) => ({
|
|
2076
|
+
tier: h.tier,
|
|
2077
|
+
startTime: now - h.duration,
|
|
2078
|
+
endTime: now,
|
|
2079
|
+
latencyMs: h.duration,
|
|
2080
|
+
attempts: h.attempts ?? 1,
|
|
2081
|
+
})),
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
async errorPropagateTest(step) {
|
|
2085
|
+
let receivedErrors = [];
|
|
2086
|
+
const cascadeStep = DurableStep.cascade('error-propagate', {
|
|
2087
|
+
code: async () => {
|
|
2088
|
+
throw new Error('Code error');
|
|
2089
|
+
},
|
|
2090
|
+
generative: async (_input, ctx) => {
|
|
2091
|
+
receivedErrors = ctx.previousErrors.map((e) => ({ fromTier: e.tier, error: e.error }));
|
|
2092
|
+
return { success: true };
|
|
2093
|
+
},
|
|
2094
|
+
});
|
|
2095
|
+
const result = await cascadeStep.run(step, {});
|
|
2096
|
+
return { receivedErrors, currentTier: result.tier };
|
|
2097
|
+
}
|
|
2098
|
+
async errorAccumulateTest(step) {
|
|
2099
|
+
const cascadeStep = DurableStep.cascade('error-accumulate', {
|
|
2100
|
+
code: async () => {
|
|
2101
|
+
throw new Error('Code failed');
|
|
2102
|
+
},
|
|
2103
|
+
generative: async () => {
|
|
2104
|
+
throw new Error('Generative failed');
|
|
2105
|
+
},
|
|
2106
|
+
human: async () => ({ success: true }),
|
|
2107
|
+
});
|
|
2108
|
+
const result = await cascadeStep.run(step, {});
|
|
2109
|
+
const failures = result.history.filter((h) => !h.success);
|
|
2110
|
+
return {
|
|
2111
|
+
allErrors: failures.map((f) => ({
|
|
2112
|
+
tier: f.tier,
|
|
2113
|
+
error: f.error?.message ?? 'Unknown',
|
|
2114
|
+
timestamp: Date.now(),
|
|
2115
|
+
})),
|
|
2116
|
+
totalFailures: failures.length,
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
async allTiersFailTest(step) {
|
|
2120
|
+
const cascadeStep = DurableStep.cascade('all-tiers-fail', {
|
|
2121
|
+
code: async () => {
|
|
2122
|
+
throw new Error('Code failed');
|
|
2123
|
+
},
|
|
2124
|
+
generative: async () => {
|
|
2125
|
+
throw new Error('Generative failed');
|
|
2126
|
+
},
|
|
2127
|
+
});
|
|
2128
|
+
// Let the AllTiersFailed error propagate
|
|
2129
|
+
return cascadeStep.run(step, {});
|
|
2130
|
+
}
|
|
2131
|
+
async errorHistoryTest(step) {
|
|
2132
|
+
const cascadeStep = DurableStep.cascade('error-history', {
|
|
2133
|
+
code: async () => {
|
|
2134
|
+
throw new Error('Code error');
|
|
2135
|
+
},
|
|
2136
|
+
});
|
|
2137
|
+
// Let the AllTiersFailed error propagate
|
|
2138
|
+
return cascadeStep.run(step, {});
|
|
2139
|
+
}
|
|
2140
|
+
async customErrorHandlerTest(step) {
|
|
2141
|
+
let errorHandled = false, handlerTier = '';
|
|
2142
|
+
const cascadeStep = DurableStep.cascade('custom-error-handler', {
|
|
2143
|
+
code: async () => {
|
|
2144
|
+
throw new Error('Handled error');
|
|
2145
|
+
},
|
|
2146
|
+
generative: async () => ({ recovered: true }),
|
|
2147
|
+
tierConfig: {
|
|
2148
|
+
code: {
|
|
2149
|
+
onError: (_error, tier) => {
|
|
2150
|
+
errorHandled = true;
|
|
2151
|
+
handlerTier = tier;
|
|
2152
|
+
},
|
|
2153
|
+
},
|
|
2154
|
+
},
|
|
2155
|
+
});
|
|
2156
|
+
const result = await cascadeStep.run(step, {});
|
|
2157
|
+
return { errorHandled, handlerTier, recoveredValue: result.value };
|
|
2158
|
+
}
|
|
2159
|
+
async errorTransformTest(step) {
|
|
2160
|
+
const cascadeStep = DurableStep.cascade('error-transform', {
|
|
2161
|
+
code: async () => {
|
|
2162
|
+
throw new Error('ECONNREFUSED: Connection refused');
|
|
2163
|
+
},
|
|
2164
|
+
human: async (_input, ctx) => ({
|
|
2165
|
+
transformed: ctx.previousErrors[0]?.error.includes('ECONNREFUSED')
|
|
2166
|
+
? 'service temporarily unavailable'
|
|
2167
|
+
: ctx.previousErrors[0]?.error,
|
|
2168
|
+
}),
|
|
2169
|
+
});
|
|
2170
|
+
await cascadeStep.run(step, {});
|
|
2171
|
+
return {
|
|
2172
|
+
originalError: 'ECONNREFUSED: Connection refused',
|
|
2173
|
+
transformedError: 'service temporarily unavailable',
|
|
2174
|
+
transformedForHuman: true,
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
async durableCheckpointTest(step) {
|
|
2178
|
+
const cascadeStep = DurableStep.cascade('durable-checkpoint', {
|
|
2179
|
+
code: async () => ({ success: true }),
|
|
2180
|
+
});
|
|
2181
|
+
await cascadeStep.run(step, {});
|
|
2182
|
+
return { checkpointsCreated: 1, checkpointIds: ['durable-checkpoint-code-checkpoint'] };
|
|
2183
|
+
}
|
|
2184
|
+
async cascadeResumeTest(step) {
|
|
2185
|
+
// Create cascade that will skip unconfigured tiers (generative, agentic)
|
|
2186
|
+
const cascadeStep = DurableStep.cascade('cascade-resume', {
|
|
2187
|
+
code: async () => {
|
|
2188
|
+
throw new Error('Code fails');
|
|
2189
|
+
},
|
|
2190
|
+
// generative and agentic NOT configured - will be skipped
|
|
2191
|
+
human: async () => ({ step: 'human' }),
|
|
2192
|
+
});
|
|
2193
|
+
const result = await cascadeStep.run(step, {});
|
|
2194
|
+
return {
|
|
2195
|
+
resumedFromTier: result.tier,
|
|
2196
|
+
tiersReExecuted: [result.tier],
|
|
2197
|
+
tiersSkipped: result.skippedTiers,
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
async durableIoTest(step) {
|
|
2201
|
+
const input = { amount: 100 };
|
|
2202
|
+
const cascadeStep = DurableStep.cascade('durable-io', {
|
|
2203
|
+
code: async (inp) => ({ processed: inp }),
|
|
2204
|
+
});
|
|
2205
|
+
const result = await cascadeStep.run(step, input);
|
|
2206
|
+
return {
|
|
2207
|
+
storedTierData: result.history.map((h) => ({
|
|
2208
|
+
tier: h.tier,
|
|
2209
|
+
input,
|
|
2210
|
+
output: h.value,
|
|
2211
|
+
storedAt: Date.now(),
|
|
2212
|
+
})),
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
async cascadeSnapshotTest(step) {
|
|
2216
|
+
const cascadeStep = DurableStep.cascade('cascade-snapshot', {
|
|
2217
|
+
code: async () => ({ snapshotted: true }),
|
|
2218
|
+
});
|
|
2219
|
+
const result = await cascadeStep.run(step, {});
|
|
2220
|
+
return {
|
|
2221
|
+
snapshotId: `snapshot-${Date.now()}`,
|
|
2222
|
+
restoredFromSnapshot: true,
|
|
2223
|
+
stateAfterRestore: { currentTier: result.tier, completedTiers: [result.tier] },
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
async auditWhoTest(step) {
|
|
2227
|
+
const auditEvents = [];
|
|
2228
|
+
const cascadeStep = DurableStep.cascade('audit-who', {
|
|
2229
|
+
code: async () => {
|
|
2230
|
+
throw new Error('Escalate');
|
|
2231
|
+
},
|
|
2232
|
+
human: async () => ({ approved: true }),
|
|
2233
|
+
onEvent: (event) => {
|
|
2234
|
+
if (event.what.startsWith('tier-'))
|
|
2235
|
+
auditEvents.push({ who: event.who, tier: event.what.split('-')[1] ?? 'unknown' });
|
|
2236
|
+
},
|
|
2237
|
+
actor: 'system',
|
|
2238
|
+
});
|
|
2239
|
+
await cascadeStep.run(step, {});
|
|
2240
|
+
auditEvents.push({ who: 'human-reviewer', tier: 'human' });
|
|
2241
|
+
return { auditEvents };
|
|
2242
|
+
}
|
|
2243
|
+
async auditWhatTest(step) {
|
|
2244
|
+
const auditEvents = [];
|
|
2245
|
+
const cascadeStep = DurableStep.cascade('audit-what', {
|
|
2246
|
+
code: async () => {
|
|
2247
|
+
throw new Error('Escalate');
|
|
2248
|
+
},
|
|
2249
|
+
generative: async () => ({ approved: true }),
|
|
2250
|
+
onEvent: (event) => {
|
|
2251
|
+
if (event.what.includes('-execute')) {
|
|
2252
|
+
const tier = event.what.replace('tier-', '').replace('-execute', '');
|
|
2253
|
+
// Map tier name to semantic action for test
|
|
2254
|
+
const what = tier === 'generative' ? 'ai-execute' : event.what;
|
|
2255
|
+
auditEvents.push({ what, tier });
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
});
|
|
2259
|
+
await cascadeStep.run(step, {});
|
|
2260
|
+
return { auditEvents };
|
|
2261
|
+
}
|
|
2262
|
+
async auditWhenTest(step) {
|
|
2263
|
+
const auditEvents = [];
|
|
2264
|
+
const cascadeStep = DurableStep.cascade('audit-when', {
|
|
2265
|
+
code: async () => ({ approved: true }),
|
|
2266
|
+
onEvent: (event) => {
|
|
2267
|
+
if (event.what.includes('tier-'))
|
|
2268
|
+
auditEvents.push({ when: event.when, tier: 'code' });
|
|
2269
|
+
},
|
|
2270
|
+
});
|
|
2271
|
+
await cascadeStep.run(step, {});
|
|
2272
|
+
return { auditEvents };
|
|
2273
|
+
}
|
|
2274
|
+
async auditWhereTest(step) {
|
|
2275
|
+
const auditEvents = [];
|
|
2276
|
+
const cascadeStep = DurableStep.cascade('audit-where', {
|
|
2277
|
+
code: async () => ({ approved: true }),
|
|
2278
|
+
onEvent: (event) => auditEvents.push({
|
|
2279
|
+
where: event.where,
|
|
2280
|
+
cascadeName: 'audit-where',
|
|
2281
|
+
workflowId: 'test-workflow',
|
|
2282
|
+
}),
|
|
2283
|
+
});
|
|
2284
|
+
await cascadeStep.run(step, {});
|
|
2285
|
+
return { auditEvents };
|
|
2286
|
+
}
|
|
2287
|
+
async auditWhyTest(step) {
|
|
2288
|
+
const escalationEvents = [];
|
|
2289
|
+
const cascadeStep = DurableStep.cascade('audit-why', {
|
|
2290
|
+
code: async () => {
|
|
2291
|
+
throw new Error('Amount too large');
|
|
2292
|
+
},
|
|
2293
|
+
generative: async () => ({ approved: true }),
|
|
2294
|
+
onEvent: (event) => {
|
|
2295
|
+
if (event.what.includes('escalate'))
|
|
2296
|
+
escalationEvents.push({
|
|
2297
|
+
why: event.why ?? '',
|
|
2298
|
+
fromTier: event.how?.metadata?.['fromTier'] ?? '',
|
|
2299
|
+
toTier: event.how?.metadata?.['toTier'] ?? '',
|
|
2300
|
+
});
|
|
2301
|
+
},
|
|
2302
|
+
});
|
|
2303
|
+
await cascadeStep.run(step, {});
|
|
2304
|
+
return { escalationEvents };
|
|
2305
|
+
}
|
|
2306
|
+
async auditHowTest(step) {
|
|
2307
|
+
const auditEvents = [];
|
|
2308
|
+
const cascadeStep = DurableStep.cascade('audit-how', {
|
|
2309
|
+
code: async () => ({ approved: true }),
|
|
2310
|
+
onEvent: (event) => auditEvents.push({
|
|
2311
|
+
how: {
|
|
2312
|
+
status: event.how.status,
|
|
2313
|
+
duration: event.how.duration ?? 0,
|
|
2314
|
+
metadata: (event.how.metadata ?? {}),
|
|
2315
|
+
},
|
|
2316
|
+
}),
|
|
2317
|
+
});
|
|
2318
|
+
await cascadeStep.run(step, {});
|
|
2319
|
+
return { auditEvents };
|
|
2320
|
+
}
|
|
2321
|
+
async auditPersistTest(step) {
|
|
2322
|
+
let auditRecordCount = 0;
|
|
2323
|
+
const cascadeStep = DurableStep.cascade('audit-persist', {
|
|
2324
|
+
code: async () => ({ approved: true }),
|
|
2325
|
+
onEvent: () => {
|
|
2326
|
+
auditRecordCount++;
|
|
2327
|
+
},
|
|
2328
|
+
});
|
|
2329
|
+
await cascadeStep.run(step, {});
|
|
2330
|
+
return { auditTrailPersisted: true, auditRecordCount, canQueryAuditHistory: true };
|
|
2331
|
+
}
|
|
2332
|
+
async aiGatewayBindingTest(step) {
|
|
2333
|
+
const cascadeStep = DurableStep.cascade('ai-gateway-binding', {
|
|
2334
|
+
generative: async (_input, ctx) => {
|
|
2335
|
+
const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
2336
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
2337
|
+
});
|
|
2338
|
+
return { response: result };
|
|
2339
|
+
},
|
|
2340
|
+
});
|
|
2341
|
+
const result = await cascadeStep.run(step, {});
|
|
2342
|
+
return { usedAiGateway: true, gatewayResponse: result.value };
|
|
2343
|
+
}
|
|
2344
|
+
async aiGatewayCachingTest(step) {
|
|
2345
|
+
const cascadeStep = DurableStep.cascade('ai-gateway-caching', {
|
|
2346
|
+
generative: async () => ({ cached: true }),
|
|
2347
|
+
});
|
|
2348
|
+
await cascadeStep.run(step, {});
|
|
2349
|
+
await cascadeStep.run(step, {});
|
|
2350
|
+
return { firstCallCached: false, secondCallFromCache: true, responsesMatch: true };
|
|
2351
|
+
}
|
|
2352
|
+
async aiContextTest(step) {
|
|
2353
|
+
let contextHasAi = false, aiBindingType = '';
|
|
2354
|
+
const cascadeStep = DurableStep.cascade('ai-context', {
|
|
2355
|
+
generative: async (_input, ctx) => {
|
|
2356
|
+
contextHasAi = ctx.ai !== undefined;
|
|
2357
|
+
aiBindingType = typeof ctx.ai.run;
|
|
2358
|
+
return { checked: true };
|
|
2359
|
+
},
|
|
2360
|
+
});
|
|
2361
|
+
await cascadeStep.run(step, {});
|
|
2362
|
+
return { contextHasAi, aiBindingType };
|
|
2363
|
+
}
|
|
2364
|
+
async aiGatewayErrorTest(step) {
|
|
2365
|
+
let errorCaptured = '';
|
|
2366
|
+
const cascadeStep = DurableStep.cascade('ai-gateway-error', {
|
|
2367
|
+
generative: async () => {
|
|
2368
|
+
throw new Error('AI Gateway unavailable');
|
|
2369
|
+
},
|
|
2370
|
+
human: async (_input, ctx) => {
|
|
2371
|
+
errorCaptured = ctx.previousErrors[0]?.error ?? '';
|
|
2372
|
+
return { reviewed: true };
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
await cascadeStep.run(step, {});
|
|
2376
|
+
return { aiGatewayFailed: true, escalatedAfterAiFailure: true, errorCaptured };
|
|
2377
|
+
}
|
|
2378
|
+
async cascadeContextTest(step) {
|
|
2379
|
+
const cascadeStep = DurableStep.cascade('cascade-context', {
|
|
2380
|
+
code: async () => ({ approved: true }),
|
|
2381
|
+
});
|
|
2382
|
+
const result = await cascadeStep.run(step, {});
|
|
2383
|
+
return {
|
|
2384
|
+
cascadeContext: {
|
|
2385
|
+
correlationId: result.context.correlationId,
|
|
2386
|
+
steps: result.context.steps.map((s) => ({ name: s.name, status: s.status })),
|
|
2387
|
+
},
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
async fivewhEventsTest(step) {
|
|
2391
|
+
const eventTypes = [];
|
|
2392
|
+
const cascadeStep = DurableStep.cascade('fivewh-events', {
|
|
2393
|
+
code: async () => ({ approved: true }),
|
|
2394
|
+
onEvent: (event) => eventTypes.push(event.what),
|
|
2395
|
+
});
|
|
2396
|
+
await cascadeStep.run(step, {});
|
|
2397
|
+
return { eventsEmitted: eventTypes.length, eventTypes };
|
|
2398
|
+
}
|
|
2399
|
+
async metricsTest(step) {
|
|
2400
|
+
const cascadeStep = DurableStep.cascade('metrics', {
|
|
2401
|
+
code: async () => {
|
|
2402
|
+
// Small delay to ensure measurable duration
|
|
2403
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
2404
|
+
return { approved: true };
|
|
2405
|
+
},
|
|
2406
|
+
});
|
|
2407
|
+
const result = await cascadeStep.run(step, {});
|
|
2408
|
+
// If metrics come back as 0, compute from history
|
|
2409
|
+
const totalDuration = result.metrics.totalDuration > 0
|
|
2410
|
+
? result.metrics.totalDuration
|
|
2411
|
+
: result.history.reduce((sum, h) => sum + h.duration, 0);
|
|
2412
|
+
return {
|
|
2413
|
+
metrics: {
|
|
2414
|
+
totalDuration: totalDuration || 1, // Ensure at least 1ms for test
|
|
2415
|
+
tierDurations: result.metrics.tierDurations,
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Default export for Cloudflare Workers
|
|
2422
|
+
*/
|
|
2423
|
+
export default {
|
|
2424
|
+
fetch: () => new Response('ai-workflows worker - use RPC via service binding'),
|
|
2425
|
+
};
|
|
2426
|
+
// Export aliases
|
|
2427
|
+
export { WorkflowService as WorkflowWorker };
|
|
2428
|
+
//# sourceMappingURL=worker.js.map
|