@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.
Files changed (84) hide show
  1. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  2. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  3. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  4. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  5. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  6. package/.github/CODEOWNERS +15 -0
  7. package/.github/dependabot.yml +23 -0
  8. package/.github/workflows/ci.yml +38 -0
  9. package/.github/workflows/publish.yml +11 -5
  10. package/.github/workflows/security.yml +29 -0
  11. package/CHANGELOG.md +34 -0
  12. package/README.md +24 -0
  13. package/SECURITY.md +40 -0
  14. package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
  15. package/dist/chunk-4VOKMGP6.js.map +1 -0
  16. package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
  17. package/dist/chunk-GYPNJBDP.js.map +1 -0
  18. package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
  19. package/dist/chunk-KHTUWLVZ.js.map +1 -0
  20. package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
  21. package/dist/chunk-ML35PIHX.js.map +1 -0
  22. package/dist/events/index.d.ts +4 -4
  23. package/dist/events/index.js +1 -1
  24. package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
  25. package/dist/index.d.ts +14 -12
  26. package/dist/index.js +40 -82
  27. package/dist/index.js.map +1 -1
  28. package/dist/scheduler/index.d.ts +4 -4
  29. package/dist/scheduler/index.js +2 -2
  30. package/dist/storage/index.d.ts +13 -13
  31. package/dist/storage/index.js +2 -2
  32. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  33. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  34. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  35. package/docs/stepflow-api-reference.md +2 -3
  36. package/package.json +7 -6
  37. package/src/core/engine.test.ts +466 -522
  38. package/src/core/engine.ts +42 -67
  39. package/src/core/index.ts +3 -3
  40. package/src/core/orchestrator.ts +64 -83
  41. package/src/core/types.ts +7 -29
  42. package/src/events/index.ts +4 -4
  43. package/src/events/memory.test.ts +39 -41
  44. package/src/events/memory.ts +7 -10
  45. package/src/events/socketio.test.ts +112 -162
  46. package/src/events/socketio.ts +20 -27
  47. package/src/events/types.ts +16 -19
  48. package/src/events/webhook.test.ts +197 -235
  49. package/src/events/webhook.ts +44 -71
  50. package/src/index.ts +19 -19
  51. package/src/planning/index.ts +6 -3
  52. package/src/planning/planner.ts +66 -91
  53. package/src/planning/planning.test.ts +303 -366
  54. package/src/planning/registry.test.ts +204 -251
  55. package/src/planning/registry.ts +39 -43
  56. package/src/planning/types.ts +23 -27
  57. package/src/scheduler/cron.test.ts +141 -150
  58. package/src/scheduler/cron.ts +32 -67
  59. package/src/scheduler/index.ts +4 -14
  60. package/src/scheduler/postgres-persistence.ts +71 -99
  61. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  62. package/src/scheduler/sqlite-persistence.ts +12 -19
  63. package/src/scheduler/types.ts +4 -9
  64. package/src/storage/index.ts +5 -5
  65. package/src/storage/memory.test.ts +133 -183
  66. package/src/storage/memory.ts +21 -42
  67. package/src/storage/postgres.ts +197 -393
  68. package/src/storage/sqlite.test.ts +154 -281
  69. package/src/storage/sqlite.ts +50 -85
  70. package/src/storage/types.ts +33 -58
  71. package/src/utils/errors.test.ts +78 -80
  72. package/src/utils/errors.ts +27 -57
  73. package/src/utils/id.ts +3 -3
  74. package/src/utils/index.ts +4 -4
  75. package/src/utils/logger.test.ts +52 -58
  76. package/src/utils/logger.ts +4 -4
  77. package/src/utils/postgres-deps.ts +4 -4
  78. package/src/utils/retry.test.ts +47 -67
  79. package/src/utils/retry.ts +9 -9
  80. package/dist/chunk-FQCT3I2A.js.map +0 -1
  81. package/dist/chunk-GXY66V7Q.js.map +0 -1
  82. package/dist/chunk-U5QGN3RT.js.map +0 -1
  83. package/dist/chunk-XCF4PLN7.js.map +0 -1
  84. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -13,18 +13,12 @@ import type {
13
13
  RunResult,
14
14
  Logger,
15
15
  SpawnChildOptions,
16
- } from "./types";
17
- import type { StorageAdapter } from "../storage/types";
18
- import type { EventTransport, WorkflowEvent } from "../events/types";
19
- import {
20
- WorkflowEngineError,
21
- StepError,
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: "running",
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 ? "run.resumed" : "run.started",
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: "", // Will be set for each step before handler is called
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: "step.skipped",
152
+ eventType: 'step.skipped',
165
153
  stepKey: step.key,
166
154
  timestamp: new Date(),
167
- payload: { reason: "checkpoint" },
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: "step.skipped",
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
- // Save checkpoint (accumulated results)
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: "succeeded",
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: "succeeded",
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: "run.completed",
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
- !isTimeout && actualError instanceof WorkflowCanceledError;
258
- const status = isCanceled ? "canceled" : "failed";
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("afterRun hook failed:", hookError);
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 ?? "fail";
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: "running",
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: "step.started",
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: "succeeded",
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: "step.completed",
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 ? "canceled" : "failed",
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: "step.failed",
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("onStepError hook failed:", hookError);
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: "step.failed",
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 === "skip") {
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: "step.skipped",
448
+ eventType: 'step.skipped',
462
449
  stepKey: step.key,
463
450
  timestamp: new Date(),
464
- payload: { reason: "error", error: lastError.message },
451
+ payload: { reason: 'error', error: lastError.message },
465
452
  });
466
453
  return undefined;
467
454
  }
468
455
 
469
- if (onError === "retry" && attempt <= maxRetries) {
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: "step.retry",
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: "canceled",
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("run"));
516
+ reject(new WorkflowCanceledError('run'));
532
517
  };
533
- signal.addEventListener("abort", onAbort, { once: true });
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("abort", onAbort);
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("run");
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("run"));
568
- signal.addEventListener("abort", onAbort, { once: true });
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("abort", onAbort);
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("Failed to emit event:", 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 = "fail" | "retry" | "skip";
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: "succeeded" | "failed" | "canceled";
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
  // ============================================================================
@@ -2,7 +2,7 @@
2
2
  * Events module exports.
3
3
  */
4
4
 
5
- export * from "./types";
6
- export * from "./memory";
7
- export * from "./socketio";
8
- export * from "./webhook";
5
+ export * from './types';
6
+ export * from './memory';
7
+ export * from './socketio';
8
+ export * from './webhook';
@@ -1,96 +1,94 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import { MemoryEventTransport } from "./memory";
3
- import type { WorkflowEvent, WorkflowEventType } from "./types";
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: "run.started",
10
- runId: "run-123",
11
- kind: "test.workflow",
12
- timestamp: new Date("2024-01-01T12:00:00Z"),
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("MemoryEventTransport", () => {
15
+ describe('MemoryEventTransport', () => {
18
16
  let transport: MemoryEventTransport;
19
17
 
20
18
  beforeEach(() => {
21
19
  transport = new MemoryEventTransport();
22
20
  });
23
21
 
24
- describe("subscribeToType", () => {
25
- it("should call callback when event of matching type is emitted", () => {
22
+ describe('subscribeToType', () => {
23
+ it('should call callback when event of matching type is emitted', () => {
26
24
  const callback = vi.fn();
27
- transport.subscribeToType("run.completed", callback);
25
+ transport.subscribeToType('run.completed', callback);
28
26
 
29
- const event = createTestEvent({ eventType: "run.completed" });
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("should not call callback for events of a different type", () => {
33
+ it('should not call callback for events of a different type', () => {
36
34
  const callback = vi.fn();
37
- transport.subscribeToType("run.completed", callback);
35
+ transport.subscribeToType('run.completed', callback);
38
36
 
39
- transport.emit(createTestEvent({ eventType: "run.started" }));
37
+ transport.emit(createTestEvent({ eventType: 'run.started' }));
40
38
 
41
39
  expect(callback).not.toHaveBeenCalled();
42
40
  });
43
41
 
44
- it("should unsubscribe when calling the returned function", () => {
42
+ it('should unsubscribe when calling the returned function', () => {
45
43
  const callback = vi.fn();
46
- const unsubscribe = transport.subscribeToType("run.completed", callback);
44
+ const unsubscribe = transport.subscribeToType('run.completed', callback);
47
45
 
48
46
  // Emit once before unsubscribe
49
- transport.emit(createTestEvent({ eventType: "run.completed" }));
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: "run.completed" }));
53
+ transport.emit(createTestEvent({ eventType: 'run.completed' }));
56
54
  expect(callback).toHaveBeenCalledTimes(1);
57
55
  });
58
56
 
59
- it("should support multiple type subscriptions independently", () => {
57
+ it('should support multiple type subscriptions independently', () => {
60
58
  const completedCb = vi.fn();
61
59
  const failedCb = vi.fn();
62
- transport.subscribeToType("run.completed", completedCb);
63
- transport.subscribeToType("run.failed", failedCb);
60
+ transport.subscribeToType('run.completed', completedCb);
61
+ transport.subscribeToType('run.failed', failedCb);
64
62
 
65
- transport.emit(createTestEvent({ eventType: "run.completed" }));
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("getListenerCount", () => {
73
- it("should return 0 when no listeners are registered", () => {
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("should return global listener count when called without channel", () => {
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("should return channel-specific listener count when called with channel", () => {
85
- transport.subscribe("run-1", vi.fn());
86
- transport.subscribe("run-1", vi.fn());
87
- transport.subscribe("run-2", vi.fn());
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("run:run-1")).toBe(2);
90
- expect(transport.getListenerCount("run:run-2")).toBe(1);
87
+ expect(transport.getListenerCount('run:run-1')).toBe(2);
88
+ expect(transport.getListenerCount('run:run-2')).toBe(1);
91
89
  });
92
90
 
93
- it("should update count after subscribe and unsubscribe", () => {
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("should return count for type-based channels", () => {
102
- transport.subscribeToType("step.completed", vi.fn());
103
- transport.subscribeToType("step.completed", vi.fn());
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("type:step.completed")).toBe(2);
106
- expect(transport.getListenerCount("type:step.failed")).toBe(0);
103
+ expect(transport.getListenerCount('type:step.completed')).toBe(2);
104
+ expect(transport.getListenerCount('type:step.failed')).toBe(0);
107
105
  });
108
106
  });
109
107
  });