@multiplier-labs/stepflow 0.3.1 → 0.3.4
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/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
package/src/storage/types.ts
CHANGED
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
* Implement StorageAdapter to use your preferred database.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
WorkflowKind,
|
|
8
|
-
RunStatus,
|
|
9
|
-
StepStatus,
|
|
10
|
-
WorkflowError,
|
|
11
|
-
} from "../core/types";
|
|
6
|
+
import type { WorkflowKind, RunStatus, StepStatus, WorkflowError } from '../core/types';
|
|
12
7
|
|
|
13
8
|
// ============================================================================
|
|
14
9
|
// Extended Status Types (New)
|
|
@@ -19,25 +14,25 @@ import type {
|
|
|
19
14
|
* Adds 'pending' and 'timeout' to the core statuses.
|
|
20
15
|
*/
|
|
21
16
|
export type ExtendedRunStatus =
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
17
|
+
| 'pending'
|
|
18
|
+
| 'queued'
|
|
19
|
+
| 'running'
|
|
20
|
+
| 'succeeded'
|
|
21
|
+
| 'failed'
|
|
22
|
+
| 'canceled'
|
|
23
|
+
| 'timeout';
|
|
29
24
|
|
|
30
25
|
/**
|
|
31
26
|
* Extended status of a workflow step.
|
|
32
27
|
* Uses 'completed' instead of 'succeeded' for consistency.
|
|
33
28
|
*/
|
|
34
29
|
export type ExtendedStepStatus =
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
30
|
+
| 'pending'
|
|
31
|
+
| 'running'
|
|
32
|
+
| 'completed'
|
|
33
|
+
| 'failed'
|
|
34
|
+
| 'skipped'
|
|
35
|
+
| 'canceled';
|
|
41
36
|
|
|
42
37
|
// ============================================================================
|
|
43
38
|
// Legacy Record Types (Backward Compatible)
|
|
@@ -53,6 +48,7 @@ export interface WorkflowRunRecord {
|
|
|
53
48
|
parentRunId?: string;
|
|
54
49
|
input: Record<string, unknown>;
|
|
55
50
|
context: Record<string, unknown>;
|
|
51
|
+
completedSteps?: string[];
|
|
56
52
|
output?: Record<string, unknown>;
|
|
57
53
|
error?: { code: string; message: string };
|
|
58
54
|
metadata?: Record<string, unknown>;
|
|
@@ -87,7 +83,7 @@ export interface WorkflowEventRecord {
|
|
|
87
83
|
runId: string;
|
|
88
84
|
stepKey?: string;
|
|
89
85
|
eventType: string;
|
|
90
|
-
level:
|
|
86
|
+
level: 'info' | 'warn' | 'error';
|
|
91
87
|
payload?: unknown;
|
|
92
88
|
timestamp: Date;
|
|
93
89
|
}
|
|
@@ -189,8 +185,8 @@ export interface ListRunsOptions {
|
|
|
189
185
|
parentRunId?: string;
|
|
190
186
|
limit?: number;
|
|
191
187
|
offset?: number;
|
|
192
|
-
orderBy?:
|
|
193
|
-
orderDirection?:
|
|
188
|
+
orderBy?: 'createdAt' | 'startedAt' | 'finishedAt';
|
|
189
|
+
orderDirection?: 'asc' | 'desc';
|
|
194
190
|
}
|
|
195
191
|
|
|
196
192
|
/**
|
|
@@ -201,8 +197,8 @@ export interface ExtendedListRunsOptions {
|
|
|
201
197
|
status?: ExtendedRunStatus | ExtendedRunStatus[];
|
|
202
198
|
limit?: number;
|
|
203
199
|
offset?: number;
|
|
204
|
-
orderBy?:
|
|
205
|
-
orderDir?:
|
|
200
|
+
orderBy?: 'createdAt' | 'startedAt' | 'finishedAt';
|
|
201
|
+
orderDir?: 'asc' | 'desc';
|
|
206
202
|
}
|
|
207
203
|
|
|
208
204
|
/**
|
|
@@ -210,7 +206,7 @@ export interface ExtendedListRunsOptions {
|
|
|
210
206
|
*/
|
|
211
207
|
export interface ListEventsOptions {
|
|
212
208
|
stepKey?: string;
|
|
213
|
-
level?:
|
|
209
|
+
level?: 'info' | 'warn' | 'error';
|
|
214
210
|
limit?: number;
|
|
215
211
|
offset?: number;
|
|
216
212
|
}
|
|
@@ -238,30 +234,21 @@ export interface WorkflowStorage {
|
|
|
238
234
|
createRun(run: CreateRunInput): Promise<ExtendedWorkflowRunRecord>;
|
|
239
235
|
getRun(id: string): Promise<ExtendedWorkflowRunRecord | null>;
|
|
240
236
|
updateRun(id: string, updates: UpdateRunInput): Promise<void>;
|
|
241
|
-
listRuns(
|
|
242
|
-
options?: ExtendedListRunsOptions,
|
|
243
|
-
): Promise<PaginatedResult<ExtendedWorkflowRunRecord>>;
|
|
237
|
+
listRuns(options?: ExtendedListRunsOptions): Promise<PaginatedResult<ExtendedWorkflowRunRecord>>;
|
|
244
238
|
deleteRun(id: string): Promise<void>;
|
|
245
239
|
|
|
246
240
|
// Atomic dequeue for concurrency control
|
|
247
|
-
dequeueRun(
|
|
248
|
-
workflowKinds: string[],
|
|
249
|
-
): Promise<ExtendedWorkflowRunRecord | null>;
|
|
241
|
+
dequeueRun(workflowKinds: string[]): Promise<ExtendedWorkflowRunRecord | null>;
|
|
250
242
|
|
|
251
243
|
// Stale workflow cleanup
|
|
252
244
|
cleanupStaleRuns(defaultTimeoutMs?: number): Promise<number>;
|
|
253
245
|
markRunsAsFailed(runIds: string[], reason: string): Promise<void>;
|
|
254
246
|
|
|
255
247
|
// Step operations
|
|
256
|
-
getStepResult(
|
|
257
|
-
runId: string,
|
|
258
|
-
stepName: string,
|
|
259
|
-
): Promise<StepResult | undefined>;
|
|
248
|
+
getStepResult(runId: string, stepName: string): Promise<StepResult | undefined>;
|
|
260
249
|
getStepResults(runId: string): Promise<StepResult[]>;
|
|
261
250
|
getStepsForRun(runId: string): Promise<StepRecord[]>;
|
|
262
|
-
saveStepResult(
|
|
263
|
-
result: Omit<StepResult, "id"> & { id?: string },
|
|
264
|
-
): Promise<void>;
|
|
251
|
+
saveStepResult(result: Omit<StepResult, 'id'> & { id?: string }): Promise<void>;
|
|
265
252
|
|
|
266
253
|
// Lifecycle
|
|
267
254
|
initialize(): Promise<void>;
|
|
@@ -278,32 +265,20 @@ export interface WorkflowStorage {
|
|
|
278
265
|
*/
|
|
279
266
|
export interface StorageAdapter {
|
|
280
267
|
// Run operations
|
|
281
|
-
createRun(
|
|
282
|
-
run: Omit<WorkflowRunRecord, "id" | "createdAt">,
|
|
283
|
-
): Promise<WorkflowRunRecord>;
|
|
268
|
+
createRun(run: Omit<WorkflowRunRecord, 'id' | 'createdAt'>): Promise<WorkflowRunRecord>;
|
|
284
269
|
getRun(runId: string): Promise<WorkflowRunRecord | null>;
|
|
285
270
|
updateRun(runId: string, updates: Partial<WorkflowRunRecord>): Promise<void>;
|
|
286
|
-
listRuns(
|
|
287
|
-
options?: ListRunsOptions,
|
|
288
|
-
): Promise<PaginatedResult<WorkflowRunRecord>>;
|
|
271
|
+
listRuns(options?: ListRunsOptions): Promise<PaginatedResult<WorkflowRunRecord>>;
|
|
289
272
|
|
|
290
273
|
// Step operations
|
|
291
|
-
createStep(
|
|
292
|
-
step: Omit<WorkflowRunStepRecord, "id">,
|
|
293
|
-
): Promise<WorkflowRunStepRecord>;
|
|
274
|
+
createStep(step: Omit<WorkflowRunStepRecord, 'id'>): Promise<WorkflowRunStepRecord>;
|
|
294
275
|
getStep(stepId: string): Promise<WorkflowRunStepRecord | null>;
|
|
295
|
-
updateStep(
|
|
296
|
-
stepId: string,
|
|
297
|
-
updates: Partial<WorkflowRunStepRecord>,
|
|
298
|
-
): Promise<void>;
|
|
276
|
+
updateStep(stepId: string, updates: Partial<WorkflowRunStepRecord>): Promise<void>;
|
|
299
277
|
getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]>;
|
|
300
278
|
|
|
301
279
|
// Event operations
|
|
302
|
-
saveEvent(event: Omit<WorkflowEventRecord,
|
|
303
|
-
getEventsForRun(
|
|
304
|
-
runId: string,
|
|
305
|
-
options?: ListEventsOptions,
|
|
306
|
-
): Promise<WorkflowEventRecord[]>;
|
|
280
|
+
saveEvent(event: Omit<WorkflowEventRecord, 'id'>): Promise<void>;
|
|
281
|
+
getEventsForRun(runId: string, options?: ListEventsOptions): Promise<WorkflowEventRecord[]>;
|
|
307
282
|
|
|
308
283
|
/**
|
|
309
284
|
* Optional transaction support.
|
|
@@ -363,6 +338,6 @@ export interface StepflowStepResultsTable {
|
|
|
363
338
|
* Combined database schema for Stepflow.
|
|
364
339
|
*/
|
|
365
340
|
export interface StepflowDatabase {
|
|
366
|
-
|
|
367
|
-
|
|
341
|
+
'stepflow.runs': StepflowRunsTable;
|
|
342
|
+
'stepflow.step_results': StepflowStepResultsTable;
|
|
368
343
|
}
|
package/src/utils/errors.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
WorkflowEngineError,
|
|
4
4
|
WorkflowNotFoundError,
|
|
@@ -8,145 +8,143 @@ import {
|
|
|
8
8
|
StepTimeoutError,
|
|
9
9
|
WorkflowCanceledError,
|
|
10
10
|
WorkflowTimeoutError,
|
|
11
|
-
} from
|
|
11
|
+
} from './errors';
|
|
12
12
|
|
|
13
|
-
describe(
|
|
14
|
-
it(
|
|
15
|
-
const err = new WorkflowEngineError(
|
|
16
|
-
key: "val",
|
|
17
|
-
});
|
|
13
|
+
describe('WorkflowEngineError', () => {
|
|
14
|
+
it('should store code, message, and details', () => {
|
|
15
|
+
const err = new WorkflowEngineError('TEST_CODE', 'test message', { key: 'val' });
|
|
18
16
|
|
|
19
|
-
expect(err.code).toBe(
|
|
20
|
-
expect(err.message).toBe(
|
|
21
|
-
expect(err.details).toEqual({ key:
|
|
22
|
-
expect(err.name).toBe(
|
|
17
|
+
expect(err.code).toBe('TEST_CODE');
|
|
18
|
+
expect(err.message).toBe('test message');
|
|
19
|
+
expect(err.details).toEqual({ key: 'val' });
|
|
20
|
+
expect(err.name).toBe('WorkflowEngineError');
|
|
23
21
|
expect(err).toBeInstanceOf(Error);
|
|
24
22
|
});
|
|
25
23
|
|
|
26
|
-
it(
|
|
27
|
-
const err = new WorkflowEngineError(
|
|
24
|
+
it('should convert to record', () => {
|
|
25
|
+
const err = new WorkflowEngineError('CODE', 'msg', { x: 1 });
|
|
28
26
|
const record = err.toRecord();
|
|
29
27
|
|
|
30
|
-
expect(record.code).toBe(
|
|
31
|
-
expect(record.message).toBe(
|
|
28
|
+
expect(record.code).toBe('CODE');
|
|
29
|
+
expect(record.message).toBe('msg');
|
|
32
30
|
expect(record.details).toEqual({ x: 1 });
|
|
33
|
-
expect(record.stack).
|
|
31
|
+
expect(record.stack).toBeUndefined();
|
|
34
32
|
});
|
|
35
33
|
|
|
36
|
-
describe(
|
|
37
|
-
it(
|
|
38
|
-
const err = new WorkflowEngineError(
|
|
34
|
+
describe('fromError', () => {
|
|
35
|
+
it('should convert WorkflowEngineError to record', () => {
|
|
36
|
+
const err = new WorkflowEngineError('MY_CODE', 'my msg');
|
|
39
37
|
const record = WorkflowEngineError.fromError(err);
|
|
40
38
|
|
|
41
|
-
expect(record.code).toBe(
|
|
42
|
-
expect(record.message).toBe(
|
|
39
|
+
expect(record.code).toBe('MY_CODE');
|
|
40
|
+
expect(record.message).toBe('my msg');
|
|
43
41
|
});
|
|
44
42
|
|
|
45
|
-
it(
|
|
46
|
-
const err = new Error(
|
|
43
|
+
it('should convert regular Error to record with default code', () => {
|
|
44
|
+
const err = new Error('regular error');
|
|
47
45
|
const record = WorkflowEngineError.fromError(err);
|
|
48
46
|
|
|
49
|
-
expect(record.code).toBe(
|
|
50
|
-
expect(record.message).toBe(
|
|
51
|
-
expect(record.stack).
|
|
47
|
+
expect(record.code).toBe('UNKNOWN_ERROR');
|
|
48
|
+
expect(record.message).toBe('regular error');
|
|
49
|
+
expect(record.stack).toBeUndefined();
|
|
52
50
|
});
|
|
53
51
|
|
|
54
|
-
it(
|
|
55
|
-
const err = new Error(
|
|
56
|
-
const record = WorkflowEngineError.fromError(err,
|
|
52
|
+
it('should convert regular Error with custom code', () => {
|
|
53
|
+
const err = new Error('oops');
|
|
54
|
+
const record = WorkflowEngineError.fromError(err, 'CUSTOM_CODE');
|
|
57
55
|
|
|
58
|
-
expect(record.code).toBe(
|
|
56
|
+
expect(record.code).toBe('CUSTOM_CODE');
|
|
59
57
|
});
|
|
60
58
|
|
|
61
|
-
it(
|
|
62
|
-
const record = WorkflowEngineError.fromError(
|
|
59
|
+
it('should convert non-Error value to record', () => {
|
|
60
|
+
const record = WorkflowEngineError.fromError('string error');
|
|
63
61
|
|
|
64
|
-
expect(record.code).toBe(
|
|
65
|
-
expect(record.message).toBe(
|
|
62
|
+
expect(record.code).toBe('UNKNOWN_ERROR');
|
|
63
|
+
expect(record.message).toBe('string error');
|
|
66
64
|
expect(record.stack).toBeUndefined();
|
|
67
65
|
});
|
|
68
66
|
|
|
69
|
-
it(
|
|
67
|
+
it('should convert number to record', () => {
|
|
70
68
|
const record = WorkflowEngineError.fromError(42);
|
|
71
69
|
|
|
72
|
-
expect(record.message).toBe(
|
|
70
|
+
expect(record.message).toBe('42');
|
|
73
71
|
});
|
|
74
72
|
});
|
|
75
73
|
});
|
|
76
74
|
|
|
77
|
-
describe(
|
|
78
|
-
it(
|
|
79
|
-
const err = new WorkflowNotFoundError(
|
|
75
|
+
describe('WorkflowNotFoundError', () => {
|
|
76
|
+
it('should have correct code and message', () => {
|
|
77
|
+
const err = new WorkflowNotFoundError('my.workflow');
|
|
80
78
|
|
|
81
|
-
expect(err.code).toBe(
|
|
79
|
+
expect(err.code).toBe('WORKFLOW_NOT_FOUND');
|
|
82
80
|
expect(err.message).toBe('Workflow "my.workflow" is not registered');
|
|
83
|
-
expect(err.name).toBe(
|
|
84
|
-
expect(err.details).toEqual({ kind:
|
|
81
|
+
expect(err.name).toBe('WorkflowNotFoundError');
|
|
82
|
+
expect(err.details).toEqual({ kind: 'my.workflow' });
|
|
85
83
|
});
|
|
86
84
|
});
|
|
87
85
|
|
|
88
|
-
describe(
|
|
89
|
-
it(
|
|
90
|
-
const err = new WorkflowAlreadyRegisteredError(
|
|
86
|
+
describe('WorkflowAlreadyRegisteredError', () => {
|
|
87
|
+
it('should have correct code and message', () => {
|
|
88
|
+
const err = new WorkflowAlreadyRegisteredError('dup.workflow');
|
|
91
89
|
|
|
92
|
-
expect(err.code).toBe(
|
|
93
|
-
expect(err.message).toContain(
|
|
94
|
-
expect(err.name).toBe(
|
|
90
|
+
expect(err.code).toBe('WORKFLOW_ALREADY_REGISTERED');
|
|
91
|
+
expect(err.message).toContain('dup.workflow');
|
|
92
|
+
expect(err.name).toBe('WorkflowAlreadyRegisteredError');
|
|
95
93
|
});
|
|
96
94
|
});
|
|
97
95
|
|
|
98
|
-
describe(
|
|
99
|
-
it(
|
|
100
|
-
const err = new RunNotFoundError(
|
|
96
|
+
describe('RunNotFoundError', () => {
|
|
97
|
+
it('should have correct code and message', () => {
|
|
98
|
+
const err = new RunNotFoundError('run-123');
|
|
101
99
|
|
|
102
|
-
expect(err.code).toBe(
|
|
103
|
-
expect(err.message).toContain(
|
|
104
|
-
expect(err.name).toBe(
|
|
100
|
+
expect(err.code).toBe('RUN_NOT_FOUND');
|
|
101
|
+
expect(err.message).toContain('run-123');
|
|
102
|
+
expect(err.name).toBe('RunNotFoundError');
|
|
105
103
|
});
|
|
106
104
|
});
|
|
107
105
|
|
|
108
|
-
describe(
|
|
109
|
-
it(
|
|
110
|
-
const cause = new Error(
|
|
111
|
-
const err = new StepError(
|
|
106
|
+
describe('StepError', () => {
|
|
107
|
+
it('should store step key, attempt, and cause', () => {
|
|
108
|
+
const cause = new Error('root cause');
|
|
109
|
+
const err = new StepError('step1', 'step failed', 3, cause);
|
|
112
110
|
|
|
113
|
-
expect(err.code).toBe(
|
|
114
|
-
expect(err.stepKey).toBe(
|
|
111
|
+
expect(err.code).toBe('STEP_ERROR');
|
|
112
|
+
expect(err.stepKey).toBe('step1');
|
|
115
113
|
expect(err.attempt).toBe(3);
|
|
116
114
|
expect(err.cause).toBe(cause);
|
|
117
|
-
expect(err.name).toBe(
|
|
115
|
+
expect(err.name).toBe('StepError');
|
|
118
116
|
});
|
|
119
117
|
});
|
|
120
118
|
|
|
121
|
-
describe(
|
|
122
|
-
it(
|
|
123
|
-
const err = new StepTimeoutError(
|
|
119
|
+
describe('StepTimeoutError', () => {
|
|
120
|
+
it('should store step key and timeout', () => {
|
|
121
|
+
const err = new StepTimeoutError('slow_step', 5000);
|
|
124
122
|
|
|
125
|
-
expect(err.code).toBe(
|
|
126
|
-
expect(err.stepKey).toBe(
|
|
123
|
+
expect(err.code).toBe('STEP_TIMEOUT');
|
|
124
|
+
expect(err.stepKey).toBe('slow_step');
|
|
127
125
|
expect(err.timeoutMs).toBe(5000);
|
|
128
|
-
expect(err.message).toContain(
|
|
129
|
-
expect(err.name).toBe(
|
|
126
|
+
expect(err.message).toContain('5000ms');
|
|
127
|
+
expect(err.name).toBe('StepTimeoutError');
|
|
130
128
|
});
|
|
131
129
|
});
|
|
132
130
|
|
|
133
|
-
describe(
|
|
134
|
-
it(
|
|
135
|
-
const err = new WorkflowCanceledError(
|
|
131
|
+
describe('WorkflowCanceledError', () => {
|
|
132
|
+
it('should have correct code and message', () => {
|
|
133
|
+
const err = new WorkflowCanceledError('run-456');
|
|
136
134
|
|
|
137
|
-
expect(err.code).toBe(
|
|
138
|
-
expect(err.message).toContain(
|
|
139
|
-
expect(err.name).toBe(
|
|
135
|
+
expect(err.code).toBe('WORKFLOW_CANCELED');
|
|
136
|
+
expect(err.message).toContain('run-456');
|
|
137
|
+
expect(err.name).toBe('WorkflowCanceledError');
|
|
140
138
|
});
|
|
141
139
|
});
|
|
142
140
|
|
|
143
|
-
describe(
|
|
144
|
-
it(
|
|
145
|
-
const err = new WorkflowTimeoutError(
|
|
141
|
+
describe('WorkflowTimeoutError', () => {
|
|
142
|
+
it('should store timeout value', () => {
|
|
143
|
+
const err = new WorkflowTimeoutError('run-789', 30000);
|
|
146
144
|
|
|
147
|
-
expect(err.code).toBe(
|
|
145
|
+
expect(err.code).toBe('WORKFLOW_TIMEOUT');
|
|
148
146
|
expect(err.timeoutMs).toBe(30000);
|
|
149
|
-
expect(err.message).toContain(
|
|
150
|
-
expect(err.name).toBe(
|
|
147
|
+
expect(err.message).toContain('30000ms');
|
|
148
|
+
expect(err.name).toBe('WorkflowTimeoutError');
|
|
151
149
|
});
|
|
152
150
|
});
|
package/src/utils/errors.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Custom error classes for the workflow engine.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { WorkflowError } from
|
|
5
|
+
import type { WorkflowError } from '../core/types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Base error class for workflow-related errors.
|
|
@@ -11,13 +11,9 @@ export class WorkflowEngineError extends Error {
|
|
|
11
11
|
readonly code: string;
|
|
12
12
|
readonly details?: Record<string, unknown>;
|
|
13
13
|
|
|
14
|
-
constructor(
|
|
15
|
-
code: string,
|
|
16
|
-
message: string,
|
|
17
|
-
details?: Record<string, unknown>,
|
|
18
|
-
) {
|
|
14
|
+
constructor(code: string, message: string, details?: Record<string, unknown>) {
|
|
19
15
|
super(message);
|
|
20
|
-
this.name =
|
|
16
|
+
this.name = 'WorkflowEngineError';
|
|
21
17
|
this.code = code;
|
|
22
18
|
this.details = details;
|
|
23
19
|
}
|
|
@@ -29,7 +25,6 @@ export class WorkflowEngineError extends Error {
|
|
|
29
25
|
return {
|
|
30
26
|
code: this.code,
|
|
31
27
|
message: this.message,
|
|
32
|
-
stack: this.stack,
|
|
33
28
|
details: this.details,
|
|
34
29
|
};
|
|
35
30
|
}
|
|
@@ -37,10 +32,7 @@ export class WorkflowEngineError extends Error {
|
|
|
37
32
|
/**
|
|
38
33
|
* Create a WorkflowError record from any error.
|
|
39
34
|
*/
|
|
40
|
-
static fromError(
|
|
41
|
-
error: unknown,
|
|
42
|
-
defaultCode = "UNKNOWN_ERROR",
|
|
43
|
-
): WorkflowError {
|
|
35
|
+
static fromError(error: unknown, defaultCode = 'UNKNOWN_ERROR'): WorkflowError {
|
|
44
36
|
if (error instanceof WorkflowEngineError) {
|
|
45
37
|
return error.toRecord();
|
|
46
38
|
}
|
|
@@ -49,7 +41,6 @@ export class WorkflowEngineError extends Error {
|
|
|
49
41
|
return {
|
|
50
42
|
code: defaultCode,
|
|
51
43
|
message: error.message,
|
|
52
|
-
stack: error.stack,
|
|
53
44
|
};
|
|
54
45
|
}
|
|
55
46
|
|
|
@@ -65,10 +56,8 @@ export class WorkflowEngineError extends Error {
|
|
|
65
56
|
*/
|
|
66
57
|
export class WorkflowNotFoundError extends WorkflowEngineError {
|
|
67
58
|
constructor(kind: string) {
|
|
68
|
-
super(
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
this.name = "WorkflowNotFoundError";
|
|
59
|
+
super('WORKFLOW_NOT_FOUND', `Workflow "${kind}" is not registered`, { kind });
|
|
60
|
+
this.name = 'WorkflowNotFoundError';
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
63
|
|
|
@@ -77,12 +66,8 @@ export class WorkflowNotFoundError extends WorkflowEngineError {
|
|
|
77
66
|
*/
|
|
78
67
|
export class WorkflowAlreadyRegisteredError extends WorkflowEngineError {
|
|
79
68
|
constructor(kind: string) {
|
|
80
|
-
super(
|
|
81
|
-
|
|
82
|
-
`Workflow "${kind}" is already registered`,
|
|
83
|
-
{ kind },
|
|
84
|
-
);
|
|
85
|
-
this.name = "WorkflowAlreadyRegisteredError";
|
|
69
|
+
super('WORKFLOW_ALREADY_REGISTERED', `Workflow "${kind}" is already registered`, { kind });
|
|
70
|
+
this.name = 'WorkflowAlreadyRegisteredError';
|
|
86
71
|
}
|
|
87
72
|
}
|
|
88
73
|
|
|
@@ -91,8 +76,8 @@ export class WorkflowAlreadyRegisteredError extends WorkflowEngineError {
|
|
|
91
76
|
*/
|
|
92
77
|
export class RunNotFoundError extends WorkflowEngineError {
|
|
93
78
|
constructor(runId: string) {
|
|
94
|
-
super(
|
|
95
|
-
this.name =
|
|
79
|
+
super('RUN_NOT_FOUND', `Run "${runId}" not found`, { runId });
|
|
80
|
+
this.name = 'RunNotFoundError';
|
|
96
81
|
}
|
|
97
82
|
}
|
|
98
83
|
|
|
@@ -104,14 +89,9 @@ export class StepError extends WorkflowEngineError {
|
|
|
104
89
|
readonly attempt: number;
|
|
105
90
|
readonly cause?: Error;
|
|
106
91
|
|
|
107
|
-
constructor(
|
|
108
|
-
stepKey
|
|
109
|
-
|
|
110
|
-
attempt: number,
|
|
111
|
-
cause?: Error,
|
|
112
|
-
) {
|
|
113
|
-
super("STEP_ERROR", message, { stepKey, attempt });
|
|
114
|
-
this.name = "StepError";
|
|
92
|
+
constructor(stepKey: string, message: string, attempt: number, cause?: Error) {
|
|
93
|
+
super('STEP_ERROR', message, { stepKey, attempt });
|
|
94
|
+
this.name = 'StepError';
|
|
115
95
|
this.stepKey = stepKey;
|
|
116
96
|
this.attempt = attempt;
|
|
117
97
|
this.cause = cause;
|
|
@@ -126,11 +106,11 @@ export class StepTimeoutError extends WorkflowEngineError {
|
|
|
126
106
|
readonly timeoutMs: number;
|
|
127
107
|
|
|
128
108
|
constructor(stepKey: string, timeoutMs: number) {
|
|
129
|
-
super(
|
|
109
|
+
super('STEP_TIMEOUT', `Step "${stepKey}" timed out after ${timeoutMs}ms`, {
|
|
130
110
|
stepKey,
|
|
131
111
|
timeoutMs,
|
|
132
112
|
});
|
|
133
|
-
this.name =
|
|
113
|
+
this.name = 'StepTimeoutError';
|
|
134
114
|
this.stepKey = stepKey;
|
|
135
115
|
this.timeoutMs = timeoutMs;
|
|
136
116
|
}
|
|
@@ -141,10 +121,8 @@ export class StepTimeoutError extends WorkflowEngineError {
|
|
|
141
121
|
*/
|
|
142
122
|
export class WorkflowCanceledError extends WorkflowEngineError {
|
|
143
123
|
constructor(runId: string) {
|
|
144
|
-
super(
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
this.name = "WorkflowCanceledError";
|
|
124
|
+
super('WORKFLOW_CANCELED', `Workflow run "${runId}" was canceled`, { runId });
|
|
125
|
+
this.name = 'WorkflowCanceledError';
|
|
148
126
|
}
|
|
149
127
|
}
|
|
150
128
|
|
|
@@ -156,15 +134,11 @@ export class WaitForRunTimeoutError extends WorkflowEngineError {
|
|
|
156
134
|
readonly timeoutMs: number;
|
|
157
135
|
|
|
158
136
|
constructor(runId: string, timeoutMs: number) {
|
|
159
|
-
super(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
timeoutMs,
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
this.name = "WaitForRunTimeoutError";
|
|
137
|
+
super('WAIT_FOR_RUN_TIMEOUT', `Timeout waiting for run ${runId} after ${timeoutMs}ms`, {
|
|
138
|
+
runId,
|
|
139
|
+
timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
this.name = 'WaitForRunTimeoutError';
|
|
168
142
|
this.runId = runId;
|
|
169
143
|
this.timeoutMs = timeoutMs;
|
|
170
144
|
}
|
|
@@ -177,15 +151,11 @@ export class WorkflowTimeoutError extends WorkflowEngineError {
|
|
|
177
151
|
readonly timeoutMs: number;
|
|
178
152
|
|
|
179
153
|
constructor(runId: string, timeoutMs: number) {
|
|
180
|
-
super(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
timeoutMs,
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
this.name = "WorkflowTimeoutError";
|
|
154
|
+
super('WORKFLOW_TIMEOUT', `Workflow run "${runId}" timed out after ${timeoutMs}ms`, {
|
|
155
|
+
runId,
|
|
156
|
+
timeoutMs,
|
|
157
|
+
});
|
|
158
|
+
this.name = 'WorkflowTimeoutError';
|
|
189
159
|
this.timeoutMs = timeoutMs;
|
|
190
160
|
}
|
|
191
161
|
}
|
package/src/utils/id.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ID generation utilities for the workflow engine.
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Generate a unique ID for database records.
|
|
@@ -10,12 +11,11 @@ import { randomBytes } from "node:crypto";
|
|
|
10
11
|
* - Rough time-ordering (useful for debugging)
|
|
11
12
|
* - High collision resistance
|
|
12
13
|
* - URL-safe characters
|
|
13
|
-
* - Cryptographically secure random component
|
|
14
14
|
*
|
|
15
15
|
* @returns A unique identifier string (approx 16 characters)
|
|
16
16
|
*/
|
|
17
17
|
export function generateId(): string {
|
|
18
18
|
const timestamp = Date.now().toString(36);
|
|
19
|
-
const randomPart = randomBytes(6).toString(
|
|
19
|
+
const randomPart = randomBytes(6).toString('base64url');
|
|
20
20
|
return `${timestamp}${randomPart}`;
|
|
21
21
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Utilities module exports.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
5
|
+
export * from './id';
|
|
6
|
+
export * from './errors';
|
|
7
|
+
export * from './logger';
|
|
8
|
+
export * from './retry';
|
|
9
|
+
export * from './safe-json';
|
|
10
|
+
export * from './webhook-signature';
|