@multiplier-labs/stepflow 0.3.1 → 0.3.3
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/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +11 -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-4VOKMGP6.js} +23 -42
- package/dist/chunk-4VOKMGP6.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
- package/dist/chunk-GYPNJBDP.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
- package/dist/chunk-KHTUWLVZ.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
- package/dist/chunk-ML35PIHX.js.map +1 -0
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
- package/dist/index.d.ts +14 -12
- package/dist/index.js +40 -82
- 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 +13 -13
- package/dist/storage/index.js +2 -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 +2 -3
- package/package.json +7 -6
- package/src/core/engine.test.ts +466 -522
- package/src/core/engine.ts +42 -67
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator.ts +64 -83
- 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 +197 -235
- package/src/events/webhook.ts +44 -71
- package/src/index.ts +19 -19
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +66 -91
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +39 -43
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +141 -150
- package/src/scheduler/cron.ts +32 -67
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +71 -99
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +12 -19
- 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.ts +197 -393
- package/src/storage/sqlite.test.ts +154 -281
- package/src/storage/sqlite.ts +50 -85
- 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 +4 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/postgres-deps.ts +4 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- 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/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
package/src/utils/logger.test.ts
CHANGED
|
@@ -1,77 +1,71 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from
|
|
2
|
-
import { ConsoleLogger, SilentLogger, createScopedLogger } from
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ConsoleLogger, SilentLogger, createScopedLogger } from './logger';
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
6
|
-
const spy = vi.spyOn(console,
|
|
7
|
-
const logger = new ConsoleLogger(
|
|
4
|
+
describe('ConsoleLogger', () => {
|
|
5
|
+
it('should log debug messages with prefix', () => {
|
|
6
|
+
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
7
|
+
const logger = new ConsoleLogger('[test]', 'debug');
|
|
8
8
|
|
|
9
|
-
logger.debug(
|
|
9
|
+
logger.debug('hello', { extra: true });
|
|
10
10
|
|
|
11
|
-
expect(spy).toHaveBeenCalledWith(
|
|
12
|
-
extra: true,
|
|
13
|
-
});
|
|
11
|
+
expect(spy).toHaveBeenCalledWith('[test] [DEBUG]', 'hello', { extra: true });
|
|
14
12
|
spy.mockRestore();
|
|
15
13
|
});
|
|
16
14
|
|
|
17
|
-
it(
|
|
18
|
-
const spy = vi.spyOn(console,
|
|
19
|
-
const logger = new ConsoleLogger(
|
|
15
|
+
it('should log info messages with prefix', () => {
|
|
16
|
+
const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
17
|
+
const logger = new ConsoleLogger('[test]');
|
|
20
18
|
|
|
21
|
-
logger.info(
|
|
19
|
+
logger.info('info msg');
|
|
22
20
|
|
|
23
|
-
expect(spy).toHaveBeenCalledWith(
|
|
21
|
+
expect(spy).toHaveBeenCalledWith('[test] [INFO]', 'info msg');
|
|
24
22
|
spy.mockRestore();
|
|
25
23
|
});
|
|
26
24
|
|
|
27
|
-
it(
|
|
28
|
-
const spy = vi.spyOn(console,
|
|
29
|
-
const logger = new ConsoleLogger(
|
|
25
|
+
it('should log warn messages with prefix', () => {
|
|
26
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
27
|
+
const logger = new ConsoleLogger('[test]');
|
|
30
28
|
|
|
31
|
-
logger.warn(
|
|
29
|
+
logger.warn('warning');
|
|
32
30
|
|
|
33
|
-
expect(spy).toHaveBeenCalledWith(
|
|
31
|
+
expect(spy).toHaveBeenCalledWith('[test] [WARN]', 'warning');
|
|
34
32
|
spy.mockRestore();
|
|
35
33
|
});
|
|
36
34
|
|
|
37
|
-
it(
|
|
38
|
-
const spy = vi.spyOn(console,
|
|
39
|
-
const logger = new ConsoleLogger(
|
|
35
|
+
it('should log error messages with prefix', () => {
|
|
36
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
+
const logger = new ConsoleLogger('[test]');
|
|
40
38
|
|
|
41
|
-
logger.error(
|
|
39
|
+
logger.error('error msg', new Error('boom'));
|
|
42
40
|
|
|
43
|
-
expect(spy).toHaveBeenCalledWith(
|
|
44
|
-
"[test] [ERROR]",
|
|
45
|
-
"error msg",
|
|
46
|
-
expect.any(Error),
|
|
47
|
-
);
|
|
41
|
+
expect(spy).toHaveBeenCalledWith('[test] [ERROR]', 'error msg', expect.any(Error));
|
|
48
42
|
spy.mockRestore();
|
|
49
43
|
});
|
|
50
44
|
|
|
51
|
-
it(
|
|
52
|
-
const spy = vi.spyOn(console,
|
|
45
|
+
it('should use default prefix when none provided', () => {
|
|
46
|
+
const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
53
47
|
const logger = new ConsoleLogger();
|
|
54
48
|
|
|
55
|
-
logger.info(
|
|
49
|
+
logger.info('test');
|
|
56
50
|
|
|
57
|
-
expect(spy).toHaveBeenCalledWith(
|
|
51
|
+
expect(spy).toHaveBeenCalledWith('[workflow] [INFO]', 'test');
|
|
58
52
|
spy.mockRestore();
|
|
59
53
|
});
|
|
60
54
|
});
|
|
61
55
|
|
|
62
|
-
describe(
|
|
63
|
-
it(
|
|
56
|
+
describe('SilentLogger', () => {
|
|
57
|
+
it('should not throw on any log level', () => {
|
|
64
58
|
const logger = new SilentLogger();
|
|
65
59
|
|
|
66
|
-
expect(() => logger.debug(
|
|
67
|
-
expect(() => logger.info(
|
|
68
|
-
expect(() => logger.warn(
|
|
69
|
-
expect(() => logger.error(
|
|
60
|
+
expect(() => logger.debug('test')).not.toThrow();
|
|
61
|
+
expect(() => logger.info('test')).not.toThrow();
|
|
62
|
+
expect(() => logger.warn('test')).not.toThrow();
|
|
63
|
+
expect(() => logger.error('test')).not.toThrow();
|
|
70
64
|
});
|
|
71
65
|
});
|
|
72
66
|
|
|
73
|
-
describe(
|
|
74
|
-
it(
|
|
67
|
+
describe('createScopedLogger', () => {
|
|
68
|
+
it('should prefix messages with run ID', () => {
|
|
75
69
|
const calls: string[] = [];
|
|
76
70
|
const baseLogger = {
|
|
77
71
|
debug: (msg: string) => calls.push(`debug:${msg}`),
|
|
@@ -80,22 +74,22 @@ describe("createScopedLogger", () => {
|
|
|
80
74
|
error: (msg: string) => calls.push(`error:${msg}`),
|
|
81
75
|
};
|
|
82
76
|
|
|
83
|
-
const scoped = createScopedLogger(baseLogger,
|
|
77
|
+
const scoped = createScopedLogger(baseLogger, 'run-123');
|
|
84
78
|
|
|
85
|
-
scoped.debug(
|
|
86
|
-
scoped.info(
|
|
87
|
-
scoped.warn(
|
|
88
|
-
scoped.error(
|
|
79
|
+
scoped.debug('d');
|
|
80
|
+
scoped.info('i');
|
|
81
|
+
scoped.warn('w');
|
|
82
|
+
scoped.error('e');
|
|
89
83
|
|
|
90
84
|
expect(calls).toEqual([
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
'debug:[run:run-123] d',
|
|
86
|
+
'info:[run:run-123] i',
|
|
87
|
+
'warn:[run:run-123] w',
|
|
88
|
+
'error:[run:run-123] e',
|
|
95
89
|
]);
|
|
96
90
|
});
|
|
97
91
|
|
|
98
|
-
it(
|
|
92
|
+
it('should include step key when provided', () => {
|
|
99
93
|
const calls: string[] = [];
|
|
100
94
|
const baseLogger = {
|
|
101
95
|
debug: (msg: string) => calls.push(msg),
|
|
@@ -104,13 +98,13 @@ describe("createScopedLogger", () => {
|
|
|
104
98
|
error: (msg: string) => calls.push(msg),
|
|
105
99
|
};
|
|
106
100
|
|
|
107
|
-
const scoped = createScopedLogger(baseLogger,
|
|
108
|
-
scoped.info(
|
|
101
|
+
const scoped = createScopedLogger(baseLogger, 'run-1', 'step-a');
|
|
102
|
+
scoped.info('hello');
|
|
109
103
|
|
|
110
|
-
expect(calls[0]).toBe(
|
|
104
|
+
expect(calls[0]).toBe('[run:run-1][step:step-a] hello');
|
|
111
105
|
});
|
|
112
106
|
|
|
113
|
-
it(
|
|
107
|
+
it('should forward extra arguments', () => {
|
|
114
108
|
const captured: unknown[][] = [];
|
|
115
109
|
const baseLogger = {
|
|
116
110
|
debug: (...args: unknown[]) => captured.push(args),
|
|
@@ -119,9 +113,9 @@ describe("createScopedLogger", () => {
|
|
|
119
113
|
error: (...args: unknown[]) => captured.push(args),
|
|
120
114
|
};
|
|
121
115
|
|
|
122
|
-
const scoped = createScopedLogger(baseLogger,
|
|
123
|
-
scoped.info(
|
|
116
|
+
const scoped = createScopedLogger(baseLogger, 'r1');
|
|
117
|
+
scoped.info('msg', { data: 1 }, 42);
|
|
124
118
|
|
|
125
|
-
expect(captured[0]).toEqual([
|
|
119
|
+
expect(captured[0]).toEqual(['[run:r1] msg', { data: 1 }, 42]);
|
|
126
120
|
});
|
|
127
121
|
});
|
package/src/utils/logger.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Logger utilities for the workflow engine.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Logger, WorkflowError } from
|
|
5
|
+
import type { Logger, WorkflowError } from '../core/types';
|
|
6
6
|
|
|
7
7
|
/** Log level for ConsoleLogger. Levels are ordered: debug < info < warn < error. */
|
|
8
|
-
export type LogLevel =
|
|
8
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
9
9
|
|
|
10
10
|
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
11
11
|
debug: 0,
|
|
@@ -23,7 +23,7 @@ export class ConsoleLogger implements Logger {
|
|
|
23
23
|
private prefix: string;
|
|
24
24
|
private minLevel: number;
|
|
25
25
|
|
|
26
|
-
constructor(prefix =
|
|
26
|
+
constructor(prefix = '[workflow]', level: LogLevel = 'info') {
|
|
27
27
|
this.prefix = prefix;
|
|
28
28
|
this.minLevel = LOG_LEVEL_PRIORITY[level];
|
|
29
29
|
}
|
|
@@ -79,7 +79,7 @@ export function sanitizeErrorForStorage(error: WorkflowError): WorkflowError {
|
|
|
79
79
|
export function createScopedLogger(
|
|
80
80
|
logger: Logger,
|
|
81
81
|
runId: string,
|
|
82
|
-
stepKey?: string
|
|
82
|
+
stepKey?: string
|
|
83
83
|
): Logger {
|
|
84
84
|
const prefix = stepKey ? `[run:${runId}][step:${stepKey}]` : `[run:${runId}]`;
|
|
85
85
|
|