@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/core/orchestrator.ts
CHANGED
|
@@ -13,18 +13,12 @@ import type {
|
|
|
13
13
|
RunResult,
|
|
14
14
|
Logger,
|
|
15
15
|
SpawnChildOptions,
|
|
16
|
-
} from
|
|
17
|
-
import type { StorageAdapter } from
|
|
18
|
-
import type { EventTransport, WorkflowEvent } from
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
StepTimeoutError,
|
|
23
|
-
WorkflowCanceledError,
|
|
24
|
-
WorkflowTimeoutError,
|
|
25
|
-
} from "../utils/errors";
|
|
26
|
-
import { createScopedLogger } from "../utils/logger";
|
|
27
|
-
import { sleep, calculateRetryDelay } from "../utils/retry";
|
|
16
|
+
} from './types';
|
|
17
|
+
import type { StorageAdapter } from '../storage/types';
|
|
18
|
+
import type { EventTransport, WorkflowEvent } from '../events/types';
|
|
19
|
+
import { WorkflowEngineError, StepError, StepTimeoutError, WorkflowCanceledError, WorkflowTimeoutError } from '../utils/errors';
|
|
20
|
+
import { createScopedLogger } from '../utils/logger';
|
|
21
|
+
import { sleep, calculateRetryDelay } from '../utils/retry';
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
24
|
* Checkpoint data for resuming workflows.
|
|
@@ -66,9 +60,7 @@ export interface ExecuteOptions {
|
|
|
66
60
|
* Execute a workflow definition.
|
|
67
61
|
* This is the main execution logic that runs all steps sequentially.
|
|
68
62
|
*/
|
|
69
|
-
export async function executeWorkflow(
|
|
70
|
-
options: ExecuteOptions,
|
|
71
|
-
): Promise<RunResult> {
|
|
63
|
+
export async function executeWorkflow(options: ExecuteOptions): Promise<RunResult> {
|
|
72
64
|
const {
|
|
73
65
|
runId,
|
|
74
66
|
definition,
|
|
@@ -88,24 +80,25 @@ export async function executeWorkflow(
|
|
|
88
80
|
// Update run status to running (if not already)
|
|
89
81
|
// On resume, don't overwrite the original startedAt
|
|
90
82
|
await storage.updateRun(runId, {
|
|
91
|
-
status:
|
|
83
|
+
status: 'running',
|
|
92
84
|
...(isResume ? {} : { startedAt: new Date() }),
|
|
93
85
|
});
|
|
94
86
|
|
|
95
87
|
emitEvent(events, logger, {
|
|
96
88
|
runId,
|
|
97
89
|
kind: definition.kind,
|
|
98
|
-
eventType: isResume ?
|
|
90
|
+
eventType: isResume ? 'run.resumed' : 'run.started',
|
|
99
91
|
timestamp: new Date(),
|
|
100
|
-
payload: isResume
|
|
101
|
-
? { resumedFrom: Array.from(checkpoint.completedStepKeys) }
|
|
102
|
-
: undefined,
|
|
92
|
+
payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : undefined,
|
|
103
93
|
});
|
|
104
94
|
|
|
95
|
+
// Track completed step keys for checkpoint persistence
|
|
96
|
+
const completedSteps: string[] = checkpoint ? Array.from(checkpoint.completedStepKeys) : [];
|
|
97
|
+
|
|
105
98
|
// Build the execution context, using checkpoint results if resuming
|
|
106
99
|
const context: WorkflowContext = {
|
|
107
100
|
runId,
|
|
108
|
-
stepId:
|
|
101
|
+
stepId: '', // Will be set for each step before handler is called
|
|
109
102
|
kind: definition.kind,
|
|
110
103
|
input,
|
|
111
104
|
results: checkpoint?.results ? { ...checkpoint.results } : {},
|
|
@@ -130,10 +123,7 @@ export async function executeWorkflow(
|
|
|
130
123
|
|
|
131
124
|
if (definition.timeout) {
|
|
132
125
|
workflowTimeoutId = setTimeout(() => {
|
|
133
|
-
workflowTimeoutError = new WorkflowTimeoutError(
|
|
134
|
-
runId,
|
|
135
|
-
definition.timeout!,
|
|
136
|
-
);
|
|
126
|
+
workflowTimeoutError = new WorkflowTimeoutError(runId, definition.timeout!);
|
|
137
127
|
abortController.abort();
|
|
138
128
|
}, definition.timeout);
|
|
139
129
|
}
|
|
@@ -155,16 +145,14 @@ export async function executeWorkflow(
|
|
|
155
145
|
|
|
156
146
|
// Skip already completed steps when resuming
|
|
157
147
|
if (checkpoint?.completedStepKeys.has(step.key)) {
|
|
158
|
-
logger.info(
|
|
159
|
-
`Skipping step "${step.key}" (already completed in previous run)`,
|
|
160
|
-
);
|
|
148
|
+
logger.info(`Skipping step "${step.key}" (already completed in previous run)`);
|
|
161
149
|
emitEvent(events, logger, {
|
|
162
150
|
runId,
|
|
163
151
|
kind: definition.kind,
|
|
164
|
-
eventType:
|
|
152
|
+
eventType: 'step.skipped',
|
|
165
153
|
stepKey: step.key,
|
|
166
154
|
timestamp: new Date(),
|
|
167
|
-
payload: { reason:
|
|
155
|
+
payload: { reason: 'checkpoint' },
|
|
168
156
|
});
|
|
169
157
|
continue;
|
|
170
158
|
}
|
|
@@ -178,7 +166,7 @@ export async function executeWorkflow(
|
|
|
178
166
|
emitEvent(events, logger, {
|
|
179
167
|
runId,
|
|
180
168
|
kind: definition.kind,
|
|
181
|
-
eventType:
|
|
169
|
+
eventType: 'step.skipped',
|
|
182
170
|
stepKey: step.key,
|
|
183
171
|
timestamp: new Date(),
|
|
184
172
|
});
|
|
@@ -199,9 +187,13 @@ export async function executeWorkflow(
|
|
|
199
187
|
// Store result in context
|
|
200
188
|
context.results[step.key] = result;
|
|
201
189
|
|
|
202
|
-
//
|
|
190
|
+
// Track completed step
|
|
191
|
+
completedSteps.push(step.key);
|
|
192
|
+
|
|
193
|
+
// Save checkpoint (accumulated results + completed steps)
|
|
203
194
|
await storage.updateRun(runId, {
|
|
204
195
|
context: { ...context.results },
|
|
196
|
+
completedSteps: [...completedSteps],
|
|
205
197
|
});
|
|
206
198
|
}
|
|
207
199
|
|
|
@@ -213,7 +205,7 @@ export async function executeWorkflow(
|
|
|
213
205
|
|
|
214
206
|
const duration = Date.now() - startTime;
|
|
215
207
|
const runResult: RunResult = {
|
|
216
|
-
status:
|
|
208
|
+
status: 'succeeded',
|
|
217
209
|
results: context.results,
|
|
218
210
|
duration,
|
|
219
211
|
};
|
|
@@ -225,7 +217,7 @@ export async function executeWorkflow(
|
|
|
225
217
|
|
|
226
218
|
// Update run status
|
|
227
219
|
await storage.updateRun(runId, {
|
|
228
|
-
status:
|
|
220
|
+
status: 'succeeded',
|
|
229
221
|
context: context.results,
|
|
230
222
|
finishedAt: new Date(),
|
|
231
223
|
});
|
|
@@ -233,12 +225,13 @@ export async function executeWorkflow(
|
|
|
233
225
|
emitEvent(events, logger, {
|
|
234
226
|
runId,
|
|
235
227
|
kind: definition.kind,
|
|
236
|
-
eventType:
|
|
228
|
+
eventType: 'run.completed',
|
|
237
229
|
timestamp: new Date(),
|
|
238
230
|
payload: { results: context.results, duration },
|
|
239
231
|
});
|
|
240
232
|
|
|
241
233
|
return runResult;
|
|
234
|
+
|
|
242
235
|
} catch (error) {
|
|
243
236
|
// Clear workflow timeout
|
|
244
237
|
if (workflowTimeoutId) {
|
|
@@ -253,14 +246,9 @@ export async function executeWorkflow(
|
|
|
253
246
|
|
|
254
247
|
// Determine final status
|
|
255
248
|
const isTimeout = actualError instanceof WorkflowTimeoutError;
|
|
256
|
-
const isCanceled =
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const eventType = isTimeout
|
|
260
|
-
? "run.timeout"
|
|
261
|
-
: isCanceled
|
|
262
|
-
? "run.canceled"
|
|
263
|
-
: "run.failed";
|
|
249
|
+
const isCanceled = !isTimeout && actualError instanceof WorkflowCanceledError;
|
|
250
|
+
const status = isCanceled ? 'canceled' : 'failed';
|
|
251
|
+
const eventType = isTimeout ? 'run.timeout' : (isCanceled ? 'run.canceled' : 'run.failed');
|
|
264
252
|
|
|
265
253
|
const runResult: RunResult = {
|
|
266
254
|
status,
|
|
@@ -274,7 +262,7 @@ export async function executeWorkflow(
|
|
|
274
262
|
try {
|
|
275
263
|
await definition.hooks.afterRun(context, runResult);
|
|
276
264
|
} catch (hookError) {
|
|
277
|
-
logger.error(
|
|
265
|
+
logger.error('afterRun hook failed for run ' + runId + ':', hookError);
|
|
278
266
|
}
|
|
279
267
|
}
|
|
280
268
|
|
|
@@ -315,12 +303,12 @@ interface StepExecutionOptions<TInput = Record<string, unknown>> {
|
|
|
315
303
|
async function executeStep<TInput>(
|
|
316
304
|
step: WorkflowStep<TInput>,
|
|
317
305
|
context: WorkflowContext<TInput>,
|
|
318
|
-
options: StepExecutionOptions<TInput
|
|
306
|
+
options: StepExecutionOptions<TInput>
|
|
319
307
|
): Promise<unknown> {
|
|
320
308
|
const { definition, storage, events, logger, abortController } = options;
|
|
321
309
|
|
|
322
310
|
// Determine error strategy
|
|
323
|
-
const onError = step.onError ?? definition.defaultOnError ??
|
|
311
|
+
const onError = step.onError ?? definition.defaultOnError ?? 'fail';
|
|
324
312
|
const maxRetries = step.maxRetries ?? 3;
|
|
325
313
|
const retryDelay = step.retryDelay ?? 1000;
|
|
326
314
|
const retryBackoff = step.retryBackoff ?? 2;
|
|
@@ -341,7 +329,7 @@ async function executeStep<TInput>(
|
|
|
341
329
|
runId: context.runId,
|
|
342
330
|
stepKey: step.key,
|
|
343
331
|
stepName: step.name,
|
|
344
|
-
status:
|
|
332
|
+
status: 'running',
|
|
345
333
|
attempt,
|
|
346
334
|
startedAt: new Date(),
|
|
347
335
|
});
|
|
@@ -353,7 +341,7 @@ async function executeStep<TInput>(
|
|
|
353
341
|
emitEvent(events, logger, {
|
|
354
342
|
runId: context.runId,
|
|
355
343
|
kind: context.kind,
|
|
356
|
-
eventType:
|
|
344
|
+
eventType: 'step.started',
|
|
357
345
|
stepKey: step.key,
|
|
358
346
|
timestamp: new Date(),
|
|
359
347
|
payload: { attempt },
|
|
@@ -374,19 +362,19 @@ async function executeStep<TInput>(
|
|
|
374
362
|
() => step.handler(context),
|
|
375
363
|
step.timeout,
|
|
376
364
|
abortController.signal,
|
|
377
|
-
step.key
|
|
365
|
+
step.key
|
|
378
366
|
);
|
|
379
367
|
} else {
|
|
380
368
|
// Race step handler against abort signal for workflow-level timeout support
|
|
381
369
|
result = await raceWithAbort(
|
|
382
370
|
step.handler(context),
|
|
383
|
-
abortController.signal
|
|
371
|
+
abortController.signal
|
|
384
372
|
);
|
|
385
373
|
}
|
|
386
374
|
|
|
387
375
|
// Step succeeded
|
|
388
376
|
await storage.updateStep(stepRecord.id, {
|
|
389
|
-
status:
|
|
377
|
+
status: 'succeeded',
|
|
390
378
|
result,
|
|
391
379
|
finishedAt: new Date(),
|
|
392
380
|
});
|
|
@@ -399,24 +387,23 @@ async function executeStep<TInput>(
|
|
|
399
387
|
emitEvent(events, logger, {
|
|
400
388
|
runId: context.runId,
|
|
401
389
|
kind: context.kind,
|
|
402
|
-
eventType:
|
|
390
|
+
eventType: 'step.completed',
|
|
403
391
|
stepKey: step.key,
|
|
404
392
|
timestamp: new Date(),
|
|
405
393
|
payload: { result },
|
|
406
394
|
});
|
|
407
395
|
|
|
408
396
|
return result;
|
|
397
|
+
|
|
409
398
|
} catch (error) {
|
|
410
399
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
411
400
|
|
|
412
401
|
// Check if this is a cancellation (abort signal fired)
|
|
413
|
-
const isCanceled =
|
|
414
|
-
error instanceof WorkflowCanceledError ||
|
|
415
|
-
abortController.signal.aborted;
|
|
402
|
+
const isCanceled = error instanceof WorkflowCanceledError || abortController.signal.aborted;
|
|
416
403
|
|
|
417
404
|
// Update step record with error
|
|
418
405
|
await storage.updateStep(stepRecord.id, {
|
|
419
|
-
status: isCanceled ?
|
|
406
|
+
status: isCanceled ? 'canceled' : 'failed',
|
|
420
407
|
error: WorkflowEngineError.fromError(error),
|
|
421
408
|
finishedAt: new Date(),
|
|
422
409
|
});
|
|
@@ -426,7 +413,7 @@ async function executeStep<TInput>(
|
|
|
426
413
|
emitEvent(events, logger, {
|
|
427
414
|
runId: context.runId,
|
|
428
415
|
kind: context.kind,
|
|
429
|
-
eventType:
|
|
416
|
+
eventType: 'step.failed',
|
|
430
417
|
stepKey: step.key,
|
|
431
418
|
timestamp: new Date(),
|
|
432
419
|
payload: { error: lastError.message, attempt },
|
|
@@ -439,43 +426,41 @@ async function executeStep<TInput>(
|
|
|
439
426
|
try {
|
|
440
427
|
await definition.hooks.onStepError(context, step, lastError);
|
|
441
428
|
} catch (hookError) {
|
|
442
|
-
logger.error(
|
|
429
|
+
logger.error('onStepError hook failed for run ' + context.runId + ':', hookError);
|
|
443
430
|
}
|
|
444
431
|
}
|
|
445
432
|
|
|
446
433
|
emitEvent(events, logger, {
|
|
447
434
|
runId: context.runId,
|
|
448
435
|
kind: context.kind,
|
|
449
|
-
eventType:
|
|
436
|
+
eventType: 'step.failed',
|
|
450
437
|
stepKey: step.key,
|
|
451
438
|
timestamp: new Date(),
|
|
452
439
|
payload: { error: lastError.message, attempt },
|
|
453
440
|
});
|
|
454
441
|
|
|
455
442
|
// Handle error based on strategy
|
|
456
|
-
if (onError ===
|
|
443
|
+
if (onError === 'skip') {
|
|
457
444
|
logger.warn(`Step "${step.key}" failed, skipping (strategy: skip)`);
|
|
458
445
|
emitEvent(events, logger, {
|
|
459
446
|
runId: context.runId,
|
|
460
447
|
kind: context.kind,
|
|
461
|
-
eventType:
|
|
448
|
+
eventType: 'step.skipped',
|
|
462
449
|
stepKey: step.key,
|
|
463
450
|
timestamp: new Date(),
|
|
464
|
-
payload: { reason:
|
|
451
|
+
payload: { reason: 'error', error: lastError.message },
|
|
465
452
|
});
|
|
466
453
|
return undefined;
|
|
467
454
|
}
|
|
468
455
|
|
|
469
|
-
if (onError ===
|
|
456
|
+
if (onError === 'retry' && attempt <= maxRetries) {
|
|
470
457
|
const delay = calculateRetryDelay(attempt, retryDelay, retryBackoff);
|
|
471
|
-
logger.warn(
|
|
472
|
-
`Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`,
|
|
473
|
-
);
|
|
458
|
+
logger.warn(`Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
|
|
474
459
|
|
|
475
460
|
emitEvent(events, logger, {
|
|
476
461
|
runId: context.runId,
|
|
477
462
|
kind: context.kind,
|
|
478
|
-
eventType:
|
|
463
|
+
eventType: 'step.retry',
|
|
479
464
|
stepKey: step.key,
|
|
480
465
|
timestamp: new Date(),
|
|
481
466
|
payload: { attempt, maxRetries, delay, error: lastError.message },
|
|
@@ -486,7 +471,7 @@ async function executeStep<TInput>(
|
|
|
486
471
|
} catch (sleepError) {
|
|
487
472
|
if (sleepError instanceof WorkflowCanceledError) {
|
|
488
473
|
await storage.updateStep(stepRecord.id, {
|
|
489
|
-
status:
|
|
474
|
+
status: 'canceled',
|
|
490
475
|
finishedAt: new Date(),
|
|
491
476
|
});
|
|
492
477
|
}
|
|
@@ -513,7 +498,7 @@ async function executeWithTimeout<T>(
|
|
|
513
498
|
fn: () => Promise<T>,
|
|
514
499
|
timeoutMs: number,
|
|
515
500
|
signal: AbortSignal,
|
|
516
|
-
stepKey: string
|
|
501
|
+
stepKey: string
|
|
517
502
|
): Promise<T> {
|
|
518
503
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
519
504
|
let onAbort: (() => void) | undefined;
|
|
@@ -528,14 +513,14 @@ async function executeWithTimeout<T>(
|
|
|
528
513
|
|
|
529
514
|
onAbort = () => {
|
|
530
515
|
clearTimeout(timeoutId);
|
|
531
|
-
reject(new WorkflowCanceledError(
|
|
516
|
+
reject(new WorkflowCanceledError('run'));
|
|
532
517
|
};
|
|
533
|
-
signal.addEventListener(
|
|
518
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
534
519
|
}),
|
|
535
520
|
]);
|
|
536
521
|
} finally {
|
|
537
522
|
if (timeoutId) clearTimeout(timeoutId);
|
|
538
|
-
if (onAbort) signal.removeEventListener(
|
|
523
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
539
524
|
}
|
|
540
525
|
}
|
|
541
526
|
|
|
@@ -552,10 +537,10 @@ async function executeWithTimeout<T>(
|
|
|
552
537
|
*/
|
|
553
538
|
async function raceWithAbort<T>(
|
|
554
539
|
promise: Promise<T>,
|
|
555
|
-
signal: AbortSignal
|
|
540
|
+
signal: AbortSignal
|
|
556
541
|
): Promise<T> {
|
|
557
542
|
if (signal.aborted) {
|
|
558
|
-
throw new WorkflowCanceledError(
|
|
543
|
+
throw new WorkflowCanceledError('run');
|
|
559
544
|
}
|
|
560
545
|
|
|
561
546
|
let onAbort: (() => void) | undefined;
|
|
@@ -564,27 +549,23 @@ async function raceWithAbort<T>(
|
|
|
564
549
|
return await Promise.race([
|
|
565
550
|
promise,
|
|
566
551
|
new Promise<never>((_, reject) => {
|
|
567
|
-
onAbort = () => reject(new WorkflowCanceledError(
|
|
568
|
-
signal.addEventListener(
|
|
552
|
+
onAbort = () => reject(new WorkflowCanceledError('run'));
|
|
553
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
569
554
|
}),
|
|
570
555
|
]);
|
|
571
556
|
} finally {
|
|
572
|
-
if (onAbort) signal.removeEventListener(
|
|
557
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
573
558
|
}
|
|
574
559
|
}
|
|
575
560
|
|
|
576
561
|
/**
|
|
577
562
|
* Helper to emit an event.
|
|
578
563
|
*/
|
|
579
|
-
function emitEvent(
|
|
580
|
-
events: EventTransport,
|
|
581
|
-
logger: Logger,
|
|
582
|
-
event: WorkflowEvent,
|
|
583
|
-
): void {
|
|
564
|
+
function emitEvent(events: EventTransport, logger: Logger, event: WorkflowEvent): void {
|
|
584
565
|
try {
|
|
585
566
|
events.emit(event);
|
|
586
567
|
} catch (error) {
|
|
587
568
|
// Log but don't fail the workflow for event emission errors
|
|
588
|
-
logger.error(
|
|
569
|
+
logger.error('Failed to emit event:', error);
|
|
589
570
|
}
|
|
590
571
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -16,23 +16,12 @@ export type WorkflowKind = string;
|
|
|
16
16
|
/**
|
|
17
17
|
* Status of a workflow run.
|
|
18
18
|
*/
|
|
19
|
-
export type RunStatus =
|
|
20
|
-
| "queued"
|
|
21
|
-
| "running"
|
|
22
|
-
| "succeeded"
|
|
23
|
-
| "failed"
|
|
24
|
-
| "canceled";
|
|
19
|
+
export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled';
|
|
25
20
|
|
|
26
21
|
/**
|
|
27
22
|
* Status of a workflow step.
|
|
28
23
|
*/
|
|
29
|
-
export type StepStatus =
|
|
30
|
-
| "pending"
|
|
31
|
-
| "running"
|
|
32
|
-
| "succeeded"
|
|
33
|
-
| "failed"
|
|
34
|
-
| "skipped"
|
|
35
|
-
| "canceled";
|
|
24
|
+
export type StepStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped' | 'canceled';
|
|
36
25
|
|
|
37
26
|
/**
|
|
38
27
|
* Error handling strategy for a step.
|
|
@@ -40,7 +29,7 @@ export type StepStatus =
|
|
|
40
29
|
* - 'retry': Retry the step up to maxRetries times
|
|
41
30
|
* - 'skip': Mark as skipped and continue to next step
|
|
42
31
|
*/
|
|
43
|
-
export type StepErrorStrategy =
|
|
32
|
+
export type StepErrorStrategy = 'fail' | 'retry' | 'skip';
|
|
44
33
|
|
|
45
34
|
// ============================================================================
|
|
46
35
|
// Error Types
|
|
@@ -60,7 +49,7 @@ export interface WorkflowError {
|
|
|
60
49
|
* Result of a completed workflow run.
|
|
61
50
|
*/
|
|
62
51
|
export interface RunResult {
|
|
63
|
-
status:
|
|
52
|
+
status: 'succeeded' | 'failed' | 'canceled';
|
|
64
53
|
results: Record<string, unknown>;
|
|
65
54
|
error?: WorkflowError;
|
|
66
55
|
duration: number;
|
|
@@ -190,24 +179,13 @@ export interface WorkflowHooks<TInput = Record<string, unknown>> {
|
|
|
190
179
|
afterRun?: (ctx: WorkflowContext<TInput>, result: RunResult) => Promise<void>;
|
|
191
180
|
|
|
192
181
|
/** Called before each step */
|
|
193
|
-
beforeStep?: (
|
|
194
|
-
ctx: WorkflowContext<TInput>,
|
|
195
|
-
step: WorkflowStep<TInput>,
|
|
196
|
-
) => Promise<void>;
|
|
182
|
+
beforeStep?: (ctx: WorkflowContext<TInput>, step: WorkflowStep<TInput>) => Promise<void>;
|
|
197
183
|
|
|
198
184
|
/** Called after each step (success only) */
|
|
199
|
-
afterStep?: (
|
|
200
|
-
ctx: WorkflowContext<TInput>,
|
|
201
|
-
step: WorkflowStep<TInput>,
|
|
202
|
-
result: unknown,
|
|
203
|
-
) => Promise<void>;
|
|
185
|
+
afterStep?: (ctx: WorkflowContext<TInput>, step: WorkflowStep<TInput>, result: unknown) => Promise<void>;
|
|
204
186
|
|
|
205
187
|
/** Called when a step fails */
|
|
206
|
-
onStepError?: (
|
|
207
|
-
ctx: WorkflowContext<TInput>,
|
|
208
|
-
step: WorkflowStep<TInput>,
|
|
209
|
-
error: Error,
|
|
210
|
-
) => Promise<void>;
|
|
188
|
+
onStepError?: (ctx: WorkflowContext<TInput>, step: WorkflowStep<TInput>, error: Error) => Promise<void>;
|
|
211
189
|
}
|
|
212
190
|
|
|
213
191
|
// ============================================================================
|
package/src/events/index.ts
CHANGED
|
@@ -1,96 +1,94 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from
|
|
2
|
-
import { MemoryEventTransport } from
|
|
3
|
-
import type { WorkflowEvent, WorkflowEventType } from
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { MemoryEventTransport } from './memory';
|
|
3
|
+
import type { WorkflowEvent, WorkflowEventType } from './types';
|
|
4
4
|
|
|
5
|
-
function createTestEvent(
|
|
6
|
-
overrides: Partial<WorkflowEvent> = {},
|
|
7
|
-
): WorkflowEvent {
|
|
5
|
+
function createTestEvent(overrides: Partial<WorkflowEvent> = {}): WorkflowEvent {
|
|
8
6
|
return {
|
|
9
|
-
eventType:
|
|
10
|
-
runId:
|
|
11
|
-
kind:
|
|
12
|
-
timestamp: new Date(
|
|
7
|
+
eventType: 'run.started',
|
|
8
|
+
runId: 'run-123',
|
|
9
|
+
kind: 'test.workflow',
|
|
10
|
+
timestamp: new Date('2024-01-01T12:00:00Z'),
|
|
13
11
|
...overrides,
|
|
14
12
|
};
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
describe(
|
|
15
|
+
describe('MemoryEventTransport', () => {
|
|
18
16
|
let transport: MemoryEventTransport;
|
|
19
17
|
|
|
20
18
|
beforeEach(() => {
|
|
21
19
|
transport = new MemoryEventTransport();
|
|
22
20
|
});
|
|
23
21
|
|
|
24
|
-
describe(
|
|
25
|
-
it(
|
|
22
|
+
describe('subscribeToType', () => {
|
|
23
|
+
it('should call callback when event of matching type is emitted', () => {
|
|
26
24
|
const callback = vi.fn();
|
|
27
|
-
transport.subscribeToType(
|
|
25
|
+
transport.subscribeToType('run.completed', callback);
|
|
28
26
|
|
|
29
|
-
const event = createTestEvent({ eventType:
|
|
27
|
+
const event = createTestEvent({ eventType: 'run.completed' });
|
|
30
28
|
transport.emit(event);
|
|
31
29
|
|
|
32
30
|
expect(callback).toHaveBeenCalledWith(event);
|
|
33
31
|
});
|
|
34
32
|
|
|
35
|
-
it(
|
|
33
|
+
it('should not call callback for events of a different type', () => {
|
|
36
34
|
const callback = vi.fn();
|
|
37
|
-
transport.subscribeToType(
|
|
35
|
+
transport.subscribeToType('run.completed', callback);
|
|
38
36
|
|
|
39
|
-
transport.emit(createTestEvent({ eventType:
|
|
37
|
+
transport.emit(createTestEvent({ eventType: 'run.started' }));
|
|
40
38
|
|
|
41
39
|
expect(callback).not.toHaveBeenCalled();
|
|
42
40
|
});
|
|
43
41
|
|
|
44
|
-
it(
|
|
42
|
+
it('should unsubscribe when calling the returned function', () => {
|
|
45
43
|
const callback = vi.fn();
|
|
46
|
-
const unsubscribe = transport.subscribeToType(
|
|
44
|
+
const unsubscribe = transport.subscribeToType('run.completed', callback);
|
|
47
45
|
|
|
48
46
|
// Emit once before unsubscribe
|
|
49
|
-
transport.emit(createTestEvent({ eventType:
|
|
47
|
+
transport.emit(createTestEvent({ eventType: 'run.completed' }));
|
|
50
48
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
51
49
|
|
|
52
50
|
unsubscribe();
|
|
53
51
|
|
|
54
52
|
// Emit again after unsubscribe
|
|
55
|
-
transport.emit(createTestEvent({ eventType:
|
|
53
|
+
transport.emit(createTestEvent({ eventType: 'run.completed' }));
|
|
56
54
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
57
55
|
});
|
|
58
56
|
|
|
59
|
-
it(
|
|
57
|
+
it('should support multiple type subscriptions independently', () => {
|
|
60
58
|
const completedCb = vi.fn();
|
|
61
59
|
const failedCb = vi.fn();
|
|
62
|
-
transport.subscribeToType(
|
|
63
|
-
transport.subscribeToType(
|
|
60
|
+
transport.subscribeToType('run.completed', completedCb);
|
|
61
|
+
transport.subscribeToType('run.failed', failedCb);
|
|
64
62
|
|
|
65
|
-
transport.emit(createTestEvent({ eventType:
|
|
63
|
+
transport.emit(createTestEvent({ eventType: 'run.completed' }));
|
|
66
64
|
|
|
67
65
|
expect(completedCb).toHaveBeenCalledTimes(1);
|
|
68
66
|
expect(failedCb).not.toHaveBeenCalled();
|
|
69
67
|
});
|
|
70
68
|
});
|
|
71
69
|
|
|
72
|
-
describe(
|
|
73
|
-
it(
|
|
70
|
+
describe('getListenerCount', () => {
|
|
71
|
+
it('should return 0 when no listeners are registered', () => {
|
|
74
72
|
expect(transport.getListenerCount()).toBe(0);
|
|
75
73
|
});
|
|
76
74
|
|
|
77
|
-
it(
|
|
75
|
+
it('should return global listener count when called without channel', () => {
|
|
78
76
|
transport.subscribeAll(vi.fn());
|
|
79
77
|
transport.subscribeAll(vi.fn());
|
|
80
78
|
|
|
81
79
|
expect(transport.getListenerCount()).toBe(2);
|
|
82
80
|
});
|
|
83
81
|
|
|
84
|
-
it(
|
|
85
|
-
transport.subscribe(
|
|
86
|
-
transport.subscribe(
|
|
87
|
-
transport.subscribe(
|
|
82
|
+
it('should return channel-specific listener count when called with channel', () => {
|
|
83
|
+
transport.subscribe('run-1', vi.fn());
|
|
84
|
+
transport.subscribe('run-1', vi.fn());
|
|
85
|
+
transport.subscribe('run-2', vi.fn());
|
|
88
86
|
|
|
89
|
-
expect(transport.getListenerCount(
|
|
90
|
-
expect(transport.getListenerCount(
|
|
87
|
+
expect(transport.getListenerCount('run:run-1')).toBe(2);
|
|
88
|
+
expect(transport.getListenerCount('run:run-2')).toBe(1);
|
|
91
89
|
});
|
|
92
90
|
|
|
93
|
-
it(
|
|
91
|
+
it('should update count after subscribe and unsubscribe', () => {
|
|
94
92
|
const unsub = transport.subscribeAll(vi.fn());
|
|
95
93
|
expect(transport.getListenerCount()).toBe(1);
|
|
96
94
|
|
|
@@ -98,12 +96,12 @@ describe("MemoryEventTransport", () => {
|
|
|
98
96
|
expect(transport.getListenerCount()).toBe(0);
|
|
99
97
|
});
|
|
100
98
|
|
|
101
|
-
it(
|
|
102
|
-
transport.subscribeToType(
|
|
103
|
-
transport.subscribeToType(
|
|
99
|
+
it('should return count for type-based channels', () => {
|
|
100
|
+
transport.subscribeToType('step.completed', vi.fn());
|
|
101
|
+
transport.subscribeToType('step.completed', vi.fn());
|
|
104
102
|
|
|
105
|
-
expect(transport.getListenerCount(
|
|
106
|
-
expect(transport.getListenerCount(
|
|
103
|
+
expect(transport.getListenerCount('type:step.completed')).toBe(2);
|
|
104
|
+
expect(transport.getListenerCount('type:step.failed')).toBe(0);
|
|
107
105
|
});
|
|
108
106
|
});
|
|
109
107
|
});
|