@multiplier-labs/stepflow 0.3.1
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/.github/workflows/publish.yml +30 -0
- package/README.md +408 -0
- package/dist/chunk-FQCT3I2A.js +1512 -0
- package/dist/chunk-FQCT3I2A.js.map +1 -0
- package/dist/chunk-GXY66V7Q.js +111 -0
- package/dist/chunk-GXY66V7Q.js.map +1 -0
- package/dist/chunk-U5QGN3RT.js +715 -0
- package/dist/chunk-U5QGN3RT.js.map +1 -0
- package/dist/chunk-XCF4PLN7.js +575 -0
- package/dist/chunk-XCF4PLN7.js.map +1 -0
- package/dist/events/index.d.ts +346 -0
- package/dist/events/index.js +11 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index-CPfGsmrp.d.ts +567 -0
- package/dist/index.d.ts +748 -0
- package/dist/index.js +1707 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/storage/index.d.ts +402 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-BVShq7zT.d.ts +150 -0
- package/dist/types-BlwFEGzj.d.ts +276 -0
- package/dist/types-DzkEqnQJ.d.ts +75 -0
- package/docs/github-packages-setup.md +97 -0
- package/docs/stepflow-api-reference.md +1662 -0
- package/package.json +85 -0
- package/src/core/engine.test.ts +1421 -0
- package/src/core/engine.ts +717 -0
- package/src/core/index.ts +7 -0
- package/src/core/orchestrator.ts +590 -0
- package/src/core/types.ts +242 -0
- package/src/events/index.ts +8 -0
- package/src/events/memory.test.ts +109 -0
- package/src/events/memory.ts +95 -0
- package/src/events/socketio.test.ts +428 -0
- package/src/events/socketio.ts +269 -0
- package/src/events/types.ts +121 -0
- package/src/events/webhook.test.ts +695 -0
- package/src/events/webhook.ts +591 -0
- package/src/index.ts +270 -0
- package/src/planning/index.ts +52 -0
- package/src/planning/planner.ts +601 -0
- package/src/planning/planning.test.ts +790 -0
- package/src/planning/registry.test.ts +445 -0
- package/src/planning/registry.ts +369 -0
- package/src/planning/types.ts +570 -0
- package/src/scheduler/cron.test.ts +480 -0
- package/src/scheduler/cron.ts +449 -0
- package/src/scheduler/index.ts +18 -0
- package/src/scheduler/postgres-persistence.test.ts +906 -0
- package/src/scheduler/postgres-persistence.ts +546 -0
- package/src/scheduler/sqlite-persistence.test.ts +235 -0
- package/src/scheduler/sqlite-persistence.ts +268 -0
- package/src/scheduler/types.ts +78 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/memory.test.ts +399 -0
- package/src/storage/memory.ts +248 -0
- package/src/storage/postgres.ts +1550 -0
- package/src/storage/sqlite.test.ts +593 -0
- package/src/storage/sqlite.ts +662 -0
- package/src/storage/types.ts +368 -0
- package/src/utils/errors.test.ts +152 -0
- package/src/utils/errors.ts +191 -0
- package/src/utils/id.ts +21 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.test.ts +127 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/postgres-deps.ts +63 -0
- package/src/utils/retry.test.ts +197 -0
- package/src/utils/retry.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage interface types for the workflow engine.
|
|
3
|
+
* Implement StorageAdapter to use your preferred database.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowKind,
|
|
8
|
+
RunStatus,
|
|
9
|
+
StepStatus,
|
|
10
|
+
WorkflowError,
|
|
11
|
+
} from "../core/types";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Extended Status Types (New)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extended status of a workflow run.
|
|
19
|
+
* Adds 'pending' and 'timeout' to the core statuses.
|
|
20
|
+
*/
|
|
21
|
+
export type ExtendedRunStatus =
|
|
22
|
+
| "pending"
|
|
23
|
+
| "queued"
|
|
24
|
+
| "running"
|
|
25
|
+
| "succeeded"
|
|
26
|
+
| "failed"
|
|
27
|
+
| "canceled"
|
|
28
|
+
| "timeout";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extended status of a workflow step.
|
|
32
|
+
* Uses 'completed' instead of 'succeeded' for consistency.
|
|
33
|
+
*/
|
|
34
|
+
export type ExtendedStepStatus =
|
|
35
|
+
| "pending"
|
|
36
|
+
| "running"
|
|
37
|
+
| "completed"
|
|
38
|
+
| "failed"
|
|
39
|
+
| "skipped"
|
|
40
|
+
| "canceled";
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Legacy Record Types (Backward Compatible)
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stored representation of a workflow run.
|
|
48
|
+
*/
|
|
49
|
+
export interface WorkflowRunRecord {
|
|
50
|
+
id: string;
|
|
51
|
+
kind: string;
|
|
52
|
+
status: RunStatus;
|
|
53
|
+
parentRunId?: string;
|
|
54
|
+
input: Record<string, unknown>;
|
|
55
|
+
context: Record<string, unknown>;
|
|
56
|
+
output?: Record<string, unknown>;
|
|
57
|
+
error?: { code: string; message: string };
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
priority?: number;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
startedAt?: Date;
|
|
63
|
+
finishedAt?: Date;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stored representation of a workflow step execution.
|
|
68
|
+
*/
|
|
69
|
+
export interface WorkflowRunStepRecord {
|
|
70
|
+
id: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
stepKey: string;
|
|
73
|
+
stepName: string;
|
|
74
|
+
status: StepStatus;
|
|
75
|
+
attempt: number;
|
|
76
|
+
result?: unknown;
|
|
77
|
+
error?: WorkflowError;
|
|
78
|
+
startedAt?: Date;
|
|
79
|
+
finishedAt?: Date;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Stored workflow event.
|
|
84
|
+
*/
|
|
85
|
+
export interface WorkflowEventRecord {
|
|
86
|
+
id: string;
|
|
87
|
+
runId: string;
|
|
88
|
+
stepKey?: string;
|
|
89
|
+
eventType: string;
|
|
90
|
+
level: "info" | "warn" | "error";
|
|
91
|
+
payload?: unknown;
|
|
92
|
+
timestamp: Date;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// New Extended Record Types
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Extended workflow run record with additional fields.
|
|
101
|
+
* Used by new WorkflowStorage implementations.
|
|
102
|
+
*/
|
|
103
|
+
export interface ExtendedWorkflowRunRecord {
|
|
104
|
+
id: string;
|
|
105
|
+
kind: string;
|
|
106
|
+
status: ExtendedRunStatus;
|
|
107
|
+
input: Record<string, unknown>;
|
|
108
|
+
context: Record<string, unknown>;
|
|
109
|
+
output?: Record<string, unknown>;
|
|
110
|
+
error?: { code: string; message: string };
|
|
111
|
+
metadata?: Record<string, unknown>;
|
|
112
|
+
priority: number;
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
createdAt: Date;
|
|
115
|
+
startedAt?: Date;
|
|
116
|
+
finishedAt?: Date;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Step result representation.
|
|
121
|
+
*/
|
|
122
|
+
export interface StepResult {
|
|
123
|
+
id: string;
|
|
124
|
+
runId: string;
|
|
125
|
+
stepName: string;
|
|
126
|
+
status: ExtendedStepStatus;
|
|
127
|
+
output?: Record<string, unknown>;
|
|
128
|
+
error?: Record<string, unknown>;
|
|
129
|
+
attempt: number;
|
|
130
|
+
startedAt?: Date;
|
|
131
|
+
completedAt?: Date;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Step record for workflow steps.
|
|
136
|
+
*/
|
|
137
|
+
export interface StepRecord {
|
|
138
|
+
stepKey: string;
|
|
139
|
+
stepName: string;
|
|
140
|
+
status: ExtendedStepStatus;
|
|
141
|
+
result?: Record<string, unknown>;
|
|
142
|
+
error?: Record<string, unknown>;
|
|
143
|
+
attempt: number;
|
|
144
|
+
startedAt?: Date;
|
|
145
|
+
finishedAt?: Date;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Input Types (New)
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Input for creating a new workflow run.
|
|
154
|
+
*/
|
|
155
|
+
export interface CreateRunInput {
|
|
156
|
+
id?: string;
|
|
157
|
+
kind: string;
|
|
158
|
+
status: ExtendedRunStatus;
|
|
159
|
+
parentRunId?: string;
|
|
160
|
+
input: Record<string, unknown>;
|
|
161
|
+
context?: Record<string, unknown>; // Optional with default {}
|
|
162
|
+
metadata?: Record<string, unknown>;
|
|
163
|
+
priority?: number;
|
|
164
|
+
timeoutMs?: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Input for updating a workflow run.
|
|
169
|
+
*/
|
|
170
|
+
export interface UpdateRunInput {
|
|
171
|
+
status?: ExtendedRunStatus;
|
|
172
|
+
context?: Record<string, unknown>;
|
|
173
|
+
output?: Record<string, unknown>;
|
|
174
|
+
error?: { code: string; message: string };
|
|
175
|
+
startedAt?: Date;
|
|
176
|
+
finishedAt?: Date;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Query Options
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Options for listing runs.
|
|
185
|
+
*/
|
|
186
|
+
export interface ListRunsOptions {
|
|
187
|
+
kind?: WorkflowKind;
|
|
188
|
+
status?: RunStatus | RunStatus[];
|
|
189
|
+
parentRunId?: string;
|
|
190
|
+
limit?: number;
|
|
191
|
+
offset?: number;
|
|
192
|
+
orderBy?: "createdAt" | "startedAt" | "finishedAt";
|
|
193
|
+
orderDirection?: "asc" | "desc";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extended options for listing runs.
|
|
198
|
+
*/
|
|
199
|
+
export interface ExtendedListRunsOptions {
|
|
200
|
+
kind?: string;
|
|
201
|
+
status?: ExtendedRunStatus | ExtendedRunStatus[];
|
|
202
|
+
limit?: number;
|
|
203
|
+
offset?: number;
|
|
204
|
+
orderBy?: "createdAt" | "startedAt" | "finishedAt";
|
|
205
|
+
orderDir?: "asc" | "desc";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Options for listing events.
|
|
210
|
+
*/
|
|
211
|
+
export interface ListEventsOptions {
|
|
212
|
+
stepKey?: string;
|
|
213
|
+
level?: "info" | "warn" | "error";
|
|
214
|
+
limit?: number;
|
|
215
|
+
offset?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Paginated result wrapper.
|
|
220
|
+
*/
|
|
221
|
+
export interface PaginatedResult<T> {
|
|
222
|
+
items: T[];
|
|
223
|
+
total: number;
|
|
224
|
+
limit?: number;
|
|
225
|
+
offset?: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// WorkflowStorage Interface (New)
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* New storage interface for workflow persistence.
|
|
234
|
+
* Implement this interface for new implementations.
|
|
235
|
+
*/
|
|
236
|
+
export interface WorkflowStorage {
|
|
237
|
+
// Run operations
|
|
238
|
+
createRun(run: CreateRunInput): Promise<ExtendedWorkflowRunRecord>;
|
|
239
|
+
getRun(id: string): Promise<ExtendedWorkflowRunRecord | null>;
|
|
240
|
+
updateRun(id: string, updates: UpdateRunInput): Promise<void>;
|
|
241
|
+
listRuns(
|
|
242
|
+
options?: ExtendedListRunsOptions,
|
|
243
|
+
): Promise<PaginatedResult<ExtendedWorkflowRunRecord>>;
|
|
244
|
+
deleteRun(id: string): Promise<void>;
|
|
245
|
+
|
|
246
|
+
// Atomic dequeue for concurrency control
|
|
247
|
+
dequeueRun(
|
|
248
|
+
workflowKinds: string[],
|
|
249
|
+
): Promise<ExtendedWorkflowRunRecord | null>;
|
|
250
|
+
|
|
251
|
+
// Stale workflow cleanup
|
|
252
|
+
cleanupStaleRuns(defaultTimeoutMs?: number): Promise<number>;
|
|
253
|
+
markRunsAsFailed(runIds: string[], reason: string): Promise<void>;
|
|
254
|
+
|
|
255
|
+
// Step operations
|
|
256
|
+
getStepResult(
|
|
257
|
+
runId: string,
|
|
258
|
+
stepName: string,
|
|
259
|
+
): Promise<StepResult | undefined>;
|
|
260
|
+
getStepResults(runId: string): Promise<StepResult[]>;
|
|
261
|
+
getStepsForRun(runId: string): Promise<StepRecord[]>;
|
|
262
|
+
saveStepResult(
|
|
263
|
+
result: Omit<StepResult, "id"> & { id?: string },
|
|
264
|
+
): Promise<void>;
|
|
265
|
+
|
|
266
|
+
// Lifecycle
|
|
267
|
+
initialize(): Promise<void>;
|
|
268
|
+
close(): Promise<void>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Legacy Storage Adapter Interface
|
|
273
|
+
// ============================================================================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Abstract storage adapter interface.
|
|
277
|
+
* Implement this interface to use your preferred database.
|
|
278
|
+
*/
|
|
279
|
+
export interface StorageAdapter {
|
|
280
|
+
// Run operations
|
|
281
|
+
createRun(
|
|
282
|
+
run: Omit<WorkflowRunRecord, "id" | "createdAt">,
|
|
283
|
+
): Promise<WorkflowRunRecord>;
|
|
284
|
+
getRun(runId: string): Promise<WorkflowRunRecord | null>;
|
|
285
|
+
updateRun(runId: string, updates: Partial<WorkflowRunRecord>): Promise<void>;
|
|
286
|
+
listRuns(
|
|
287
|
+
options?: ListRunsOptions,
|
|
288
|
+
): Promise<PaginatedResult<WorkflowRunRecord>>;
|
|
289
|
+
|
|
290
|
+
// Step operations
|
|
291
|
+
createStep(
|
|
292
|
+
step: Omit<WorkflowRunStepRecord, "id">,
|
|
293
|
+
): Promise<WorkflowRunStepRecord>;
|
|
294
|
+
getStep(stepId: string): Promise<WorkflowRunStepRecord | null>;
|
|
295
|
+
updateStep(
|
|
296
|
+
stepId: string,
|
|
297
|
+
updates: Partial<WorkflowRunStepRecord>,
|
|
298
|
+
): Promise<void>;
|
|
299
|
+
getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]>;
|
|
300
|
+
|
|
301
|
+
// Event operations
|
|
302
|
+
saveEvent(event: Omit<WorkflowEventRecord, "id">): Promise<void>;
|
|
303
|
+
getEventsForRun(
|
|
304
|
+
runId: string,
|
|
305
|
+
options?: ListEventsOptions,
|
|
306
|
+
): Promise<WorkflowEventRecord[]>;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Optional transaction support.
|
|
310
|
+
* @deprecated Transaction support is deprecated and will be removed in a future release.
|
|
311
|
+
*/
|
|
312
|
+
transaction?<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T>;
|
|
313
|
+
|
|
314
|
+
// Optional: Cleanup
|
|
315
|
+
deleteOldRuns?(olderThan: Date): Promise<number>;
|
|
316
|
+
|
|
317
|
+
// Optional: Lifecycle (required for adapters that need async setup, e.g. PostgreSQL)
|
|
318
|
+
initialize?(): Promise<void>;
|
|
319
|
+
close?(): void | Promise<void>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// Database Schema Types
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Database table schema for workflow runs.
|
|
328
|
+
* Note: When used with Kysely, wrap auto-generated fields with Generated<T>.
|
|
329
|
+
*/
|
|
330
|
+
export interface StepflowRunsTable {
|
|
331
|
+
id: string;
|
|
332
|
+
kind: string;
|
|
333
|
+
status: ExtendedRunStatus;
|
|
334
|
+
input: Record<string, unknown>;
|
|
335
|
+
context: Record<string, unknown>;
|
|
336
|
+
output: Record<string, unknown> | null;
|
|
337
|
+
error: { code: string; message: string } | null;
|
|
338
|
+
metadata: Record<string, unknown> | null;
|
|
339
|
+
priority: number;
|
|
340
|
+
timeout_ms: number | null;
|
|
341
|
+
created_at: Date;
|
|
342
|
+
started_at: Date | null;
|
|
343
|
+
finished_at: Date | null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Database table schema for step results.
|
|
348
|
+
* Note: When used with Kysely, wrap auto-generated fields with Generated<T>.
|
|
349
|
+
*/
|
|
350
|
+
export interface StepflowStepResultsTable {
|
|
351
|
+
id: string;
|
|
352
|
+
run_id: string;
|
|
353
|
+
step_name: string;
|
|
354
|
+
status: ExtendedStepStatus;
|
|
355
|
+
output: Record<string, unknown> | null;
|
|
356
|
+
error: Record<string, unknown> | null;
|
|
357
|
+
attempt: number;
|
|
358
|
+
started_at: Date | null;
|
|
359
|
+
completed_at: Date | null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Combined database schema for Stepflow.
|
|
364
|
+
*/
|
|
365
|
+
export interface StepflowDatabase {
|
|
366
|
+
"stepflow.runs": StepflowRunsTable;
|
|
367
|
+
"stepflow.step_results": StepflowStepResultsTable;
|
|
368
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
WorkflowEngineError,
|
|
4
|
+
WorkflowNotFoundError,
|
|
5
|
+
WorkflowAlreadyRegisteredError,
|
|
6
|
+
RunNotFoundError,
|
|
7
|
+
StepError,
|
|
8
|
+
StepTimeoutError,
|
|
9
|
+
WorkflowCanceledError,
|
|
10
|
+
WorkflowTimeoutError,
|
|
11
|
+
} from "./errors";
|
|
12
|
+
|
|
13
|
+
describe("WorkflowEngineError", () => {
|
|
14
|
+
it("should store code, message, and details", () => {
|
|
15
|
+
const err = new WorkflowEngineError("TEST_CODE", "test message", {
|
|
16
|
+
key: "val",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(err.code).toBe("TEST_CODE");
|
|
20
|
+
expect(err.message).toBe("test message");
|
|
21
|
+
expect(err.details).toEqual({ key: "val" });
|
|
22
|
+
expect(err.name).toBe("WorkflowEngineError");
|
|
23
|
+
expect(err).toBeInstanceOf(Error);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should convert to record", () => {
|
|
27
|
+
const err = new WorkflowEngineError("CODE", "msg", { x: 1 });
|
|
28
|
+
const record = err.toRecord();
|
|
29
|
+
|
|
30
|
+
expect(record.code).toBe("CODE");
|
|
31
|
+
expect(record.message).toBe("msg");
|
|
32
|
+
expect(record.details).toEqual({ x: 1 });
|
|
33
|
+
expect(record.stack).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("fromError", () => {
|
|
37
|
+
it("should convert WorkflowEngineError to record", () => {
|
|
38
|
+
const err = new WorkflowEngineError("MY_CODE", "my msg");
|
|
39
|
+
const record = WorkflowEngineError.fromError(err);
|
|
40
|
+
|
|
41
|
+
expect(record.code).toBe("MY_CODE");
|
|
42
|
+
expect(record.message).toBe("my msg");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should convert regular Error to record with default code", () => {
|
|
46
|
+
const err = new Error("regular error");
|
|
47
|
+
const record = WorkflowEngineError.fromError(err);
|
|
48
|
+
|
|
49
|
+
expect(record.code).toBe("UNKNOWN_ERROR");
|
|
50
|
+
expect(record.message).toBe("regular error");
|
|
51
|
+
expect(record.stack).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should convert regular Error with custom code", () => {
|
|
55
|
+
const err = new Error("oops");
|
|
56
|
+
const record = WorkflowEngineError.fromError(err, "CUSTOM_CODE");
|
|
57
|
+
|
|
58
|
+
expect(record.code).toBe("CUSTOM_CODE");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should convert non-Error value to record", () => {
|
|
62
|
+
const record = WorkflowEngineError.fromError("string error");
|
|
63
|
+
|
|
64
|
+
expect(record.code).toBe("UNKNOWN_ERROR");
|
|
65
|
+
expect(record.message).toBe("string error");
|
|
66
|
+
expect(record.stack).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should convert number to record", () => {
|
|
70
|
+
const record = WorkflowEngineError.fromError(42);
|
|
71
|
+
|
|
72
|
+
expect(record.message).toBe("42");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("WorkflowNotFoundError", () => {
|
|
78
|
+
it("should have correct code and message", () => {
|
|
79
|
+
const err = new WorkflowNotFoundError("my.workflow");
|
|
80
|
+
|
|
81
|
+
expect(err.code).toBe("WORKFLOW_NOT_FOUND");
|
|
82
|
+
expect(err.message).toBe('Workflow "my.workflow" is not registered');
|
|
83
|
+
expect(err.name).toBe("WorkflowNotFoundError");
|
|
84
|
+
expect(err.details).toEqual({ kind: "my.workflow" });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("WorkflowAlreadyRegisteredError", () => {
|
|
89
|
+
it("should have correct code and message", () => {
|
|
90
|
+
const err = new WorkflowAlreadyRegisteredError("dup.workflow");
|
|
91
|
+
|
|
92
|
+
expect(err.code).toBe("WORKFLOW_ALREADY_REGISTERED");
|
|
93
|
+
expect(err.message).toContain("dup.workflow");
|
|
94
|
+
expect(err.name).toBe("WorkflowAlreadyRegisteredError");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("RunNotFoundError", () => {
|
|
99
|
+
it("should have correct code and message", () => {
|
|
100
|
+
const err = new RunNotFoundError("run-123");
|
|
101
|
+
|
|
102
|
+
expect(err.code).toBe("RUN_NOT_FOUND");
|
|
103
|
+
expect(err.message).toContain("run-123");
|
|
104
|
+
expect(err.name).toBe("RunNotFoundError");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("StepError", () => {
|
|
109
|
+
it("should store step key, attempt, and cause", () => {
|
|
110
|
+
const cause = new Error("root cause");
|
|
111
|
+
const err = new StepError("step1", "step failed", 3, cause);
|
|
112
|
+
|
|
113
|
+
expect(err.code).toBe("STEP_ERROR");
|
|
114
|
+
expect(err.stepKey).toBe("step1");
|
|
115
|
+
expect(err.attempt).toBe(3);
|
|
116
|
+
expect(err.cause).toBe(cause);
|
|
117
|
+
expect(err.name).toBe("StepError");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("StepTimeoutError", () => {
|
|
122
|
+
it("should store step key and timeout", () => {
|
|
123
|
+
const err = new StepTimeoutError("slow_step", 5000);
|
|
124
|
+
|
|
125
|
+
expect(err.code).toBe("STEP_TIMEOUT");
|
|
126
|
+
expect(err.stepKey).toBe("slow_step");
|
|
127
|
+
expect(err.timeoutMs).toBe(5000);
|
|
128
|
+
expect(err.message).toContain("5000ms");
|
|
129
|
+
expect(err.name).toBe("StepTimeoutError");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("WorkflowCanceledError", () => {
|
|
134
|
+
it("should have correct code and message", () => {
|
|
135
|
+
const err = new WorkflowCanceledError("run-456");
|
|
136
|
+
|
|
137
|
+
expect(err.code).toBe("WORKFLOW_CANCELED");
|
|
138
|
+
expect(err.message).toContain("run-456");
|
|
139
|
+
expect(err.name).toBe("WorkflowCanceledError");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("WorkflowTimeoutError", () => {
|
|
144
|
+
it("should store timeout value", () => {
|
|
145
|
+
const err = new WorkflowTimeoutError("run-789", 30000);
|
|
146
|
+
|
|
147
|
+
expect(err.code).toBe("WORKFLOW_TIMEOUT");
|
|
148
|
+
expect(err.timeoutMs).toBe(30000);
|
|
149
|
+
expect(err.message).toContain("30000ms");
|
|
150
|
+
expect(err.name).toBe("WorkflowTimeoutError");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for the workflow engine.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { WorkflowError } from "../core/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base error class for workflow-related errors.
|
|
9
|
+
*/
|
|
10
|
+
export class WorkflowEngineError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly details?: Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
code: string,
|
|
16
|
+
message: string,
|
|
17
|
+
details?: Record<string, unknown>,
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "WorkflowEngineError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.details = details;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert to a WorkflowError record for storage.
|
|
27
|
+
*/
|
|
28
|
+
toRecord(): WorkflowError {
|
|
29
|
+
return {
|
|
30
|
+
code: this.code,
|
|
31
|
+
message: this.message,
|
|
32
|
+
stack: this.stack,
|
|
33
|
+
details: this.details,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a WorkflowError record from any error.
|
|
39
|
+
*/
|
|
40
|
+
static fromError(
|
|
41
|
+
error: unknown,
|
|
42
|
+
defaultCode = "UNKNOWN_ERROR",
|
|
43
|
+
): WorkflowError {
|
|
44
|
+
if (error instanceof WorkflowEngineError) {
|
|
45
|
+
return error.toRecord();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
return {
|
|
50
|
+
code: defaultCode,
|
|
51
|
+
message: error.message,
|
|
52
|
+
stack: error.stack,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
code: defaultCode,
|
|
58
|
+
message: String(error),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Error thrown when a workflow is not found in the registry.
|
|
65
|
+
*/
|
|
66
|
+
export class WorkflowNotFoundError extends WorkflowEngineError {
|
|
67
|
+
constructor(kind: string) {
|
|
68
|
+
super("WORKFLOW_NOT_FOUND", `Workflow "${kind}" is not registered`, {
|
|
69
|
+
kind,
|
|
70
|
+
});
|
|
71
|
+
this.name = "WorkflowNotFoundError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Error thrown when a workflow is already registered.
|
|
77
|
+
*/
|
|
78
|
+
export class WorkflowAlreadyRegisteredError extends WorkflowEngineError {
|
|
79
|
+
constructor(kind: string) {
|
|
80
|
+
super(
|
|
81
|
+
"WORKFLOW_ALREADY_REGISTERED",
|
|
82
|
+
`Workflow "${kind}" is already registered`,
|
|
83
|
+
{ kind },
|
|
84
|
+
);
|
|
85
|
+
this.name = "WorkflowAlreadyRegisteredError";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Error thrown when a run is not found.
|
|
91
|
+
*/
|
|
92
|
+
export class RunNotFoundError extends WorkflowEngineError {
|
|
93
|
+
constructor(runId: string) {
|
|
94
|
+
super("RUN_NOT_FOUND", `Run "${runId}" not found`, { runId });
|
|
95
|
+
this.name = "RunNotFoundError";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Error thrown when a step fails.
|
|
101
|
+
*/
|
|
102
|
+
export class StepError extends WorkflowEngineError {
|
|
103
|
+
readonly stepKey: string;
|
|
104
|
+
readonly attempt: number;
|
|
105
|
+
readonly cause?: Error;
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
stepKey: string,
|
|
109
|
+
message: string,
|
|
110
|
+
attempt: number,
|
|
111
|
+
cause?: Error,
|
|
112
|
+
) {
|
|
113
|
+
super("STEP_ERROR", message, { stepKey, attempt });
|
|
114
|
+
this.name = "StepError";
|
|
115
|
+
this.stepKey = stepKey;
|
|
116
|
+
this.attempt = attempt;
|
|
117
|
+
this.cause = cause;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Error thrown when a step times out.
|
|
123
|
+
*/
|
|
124
|
+
export class StepTimeoutError extends WorkflowEngineError {
|
|
125
|
+
readonly stepKey: string;
|
|
126
|
+
readonly timeoutMs: number;
|
|
127
|
+
|
|
128
|
+
constructor(stepKey: string, timeoutMs: number) {
|
|
129
|
+
super("STEP_TIMEOUT", `Step "${stepKey}" timed out after ${timeoutMs}ms`, {
|
|
130
|
+
stepKey,
|
|
131
|
+
timeoutMs,
|
|
132
|
+
});
|
|
133
|
+
this.name = "StepTimeoutError";
|
|
134
|
+
this.stepKey = stepKey;
|
|
135
|
+
this.timeoutMs = timeoutMs;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Error thrown when a workflow is canceled.
|
|
141
|
+
*/
|
|
142
|
+
export class WorkflowCanceledError extends WorkflowEngineError {
|
|
143
|
+
constructor(runId: string) {
|
|
144
|
+
super("WORKFLOW_CANCELED", `Workflow run "${runId}" was canceled`, {
|
|
145
|
+
runId,
|
|
146
|
+
});
|
|
147
|
+
this.name = "WorkflowCanceledError";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Error thrown when waitForRun times out polling for a terminal status.
|
|
153
|
+
*/
|
|
154
|
+
export class WaitForRunTimeoutError extends WorkflowEngineError {
|
|
155
|
+
readonly runId: string;
|
|
156
|
+
readonly timeoutMs: number;
|
|
157
|
+
|
|
158
|
+
constructor(runId: string, timeoutMs: number) {
|
|
159
|
+
super(
|
|
160
|
+
"WAIT_FOR_RUN_TIMEOUT",
|
|
161
|
+
`Timeout waiting for run ${runId} after ${timeoutMs}ms`,
|
|
162
|
+
{
|
|
163
|
+
runId,
|
|
164
|
+
timeoutMs,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
this.name = "WaitForRunTimeoutError";
|
|
168
|
+
this.runId = runId;
|
|
169
|
+
this.timeoutMs = timeoutMs;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Error thrown when a workflow times out.
|
|
175
|
+
*/
|
|
176
|
+
export class WorkflowTimeoutError extends WorkflowEngineError {
|
|
177
|
+
readonly timeoutMs: number;
|
|
178
|
+
|
|
179
|
+
constructor(runId: string, timeoutMs: number) {
|
|
180
|
+
super(
|
|
181
|
+
"WORKFLOW_TIMEOUT",
|
|
182
|
+
`Workflow run "${runId}" timed out after ${timeoutMs}ms`,
|
|
183
|
+
{
|
|
184
|
+
runId,
|
|
185
|
+
timeoutMs,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
this.name = "WorkflowTimeoutError";
|
|
189
|
+
this.timeoutMs = timeoutMs;
|
|
190
|
+
}
|
|
191
|
+
}
|