@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.
Files changed (112) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  4. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  5. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  6. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  7. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  8. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  9. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  10. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  11. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  12. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  13. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  14. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  15. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  16. package/.github/CODEOWNERS +15 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/workflows/ci.yml +38 -0
  19. package/.github/workflows/publish.yml +17 -5
  20. package/.github/workflows/security.yml +29 -0
  21. package/CHANGELOG.md +34 -0
  22. package/README.md +24 -0
  23. package/SECURITY.md +40 -0
  24. package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
  25. package/dist/chunk-5XJBAPXF.js.map +1 -0
  26. package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
  27. package/dist/chunk-FMPJBK2N.js.map +1 -0
  28. package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
  29. package/dist/chunk-JPXPB7U5.js.map +1 -0
  30. package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
  31. package/dist/chunk-U3PCVK6O.js.map +1 -0
  32. package/dist/events/index.d.ts +60 -7
  33. package/dist/events/index.js +1 -1
  34. package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
  35. package/dist/index.d.ts +15 -23
  36. package/dist/index.js +784 -547
  37. package/dist/index.js.map +1 -1
  38. package/dist/scheduler/index.d.ts +4 -4
  39. package/dist/scheduler/index.js +2 -2
  40. package/dist/storage/index.d.ts +322 -56
  41. package/dist/storage/index.js +4 -2
  42. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  43. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  44. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  45. package/docs/stepflow-api-reference.md +5 -4
  46. package/package.json +12 -8
  47. package/src/core/engine/concurrency.ts +229 -0
  48. package/src/core/engine/lifecycle.ts +176 -0
  49. package/src/core/engine.test.ts +536 -522
  50. package/src/core/engine.ts +116 -235
  51. package/src/core/index.ts +3 -3
  52. package/src/core/orchestrator/persistence.ts +165 -0
  53. package/src/core/orchestrator/plan-resolution.ts +140 -0
  54. package/src/core/orchestrator/state-transitions.ts +109 -0
  55. package/src/core/orchestrator/step-runner.ts +427 -0
  56. package/src/core/orchestrator.ts +57 -395
  57. package/src/core/types.ts +7 -29
  58. package/src/events/index.ts +4 -4
  59. package/src/events/memory.test.ts +39 -41
  60. package/src/events/memory.ts +7 -10
  61. package/src/events/socketio.test.ts +112 -162
  62. package/src/events/socketio.ts +20 -27
  63. package/src/events/types.ts +16 -19
  64. package/src/events/webhook.test.ts +489 -235
  65. package/src/events/webhook.ts +355 -139
  66. package/src/index.ts +19 -19
  67. package/src/planning/evaluate-condition.test.ts +153 -0
  68. package/src/planning/evaluate-condition.ts +105 -0
  69. package/src/planning/index.ts +6 -3
  70. package/src/planning/planner.ts +45 -152
  71. package/src/planning/planning.test.ts +303 -366
  72. package/src/planning/registry.test.ts +204 -251
  73. package/src/planning/registry.ts +20 -106
  74. package/src/planning/types.ts +23 -27
  75. package/src/scheduler/cron.test.ts +149 -150
  76. package/src/scheduler/cron.ts +58 -69
  77. package/src/scheduler/index.ts +4 -14
  78. package/src/scheduler/postgres-persistence.ts +89 -105
  79. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  80. package/src/scheduler/sqlite-persistence.ts +40 -22
  81. package/src/scheduler/types.ts +4 -9
  82. package/src/storage/index.ts +5 -5
  83. package/src/storage/memory.test.ts +133 -183
  84. package/src/storage/memory.ts +21 -42
  85. package/src/storage/postgres-core.ts +550 -0
  86. package/src/storage/postgres-migration.test.ts +69 -0
  87. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  88. package/src/storage/postgres.ts +222 -937
  89. package/src/storage/sqlite-core.ts +151 -0
  90. package/src/storage/sqlite.test.ts +330 -281
  91. package/src/storage/sqlite.ts +225 -170
  92. package/src/storage/types.ts +33 -58
  93. package/src/utils/errors.test.ts +78 -80
  94. package/src/utils/errors.ts +27 -57
  95. package/src/utils/id.ts +3 -3
  96. package/src/utils/index.ts +6 -4
  97. package/src/utils/logger.test.ts +52 -58
  98. package/src/utils/logger.ts +4 -4
  99. package/src/utils/pinned-fetch.test.ts +84 -0
  100. package/src/utils/pinned-fetch.ts +118 -0
  101. package/src/utils/postgres-deps.ts +20 -4
  102. package/src/utils/retry.test.ts +47 -67
  103. package/src/utils/retry.ts +9 -9
  104. package/src/utils/safe-json.test.ts +166 -0
  105. package/src/utils/safe-json.ts +107 -0
  106. package/src/utils/webhook-signature.test.ts +111 -0
  107. package/src/utils/webhook-signature.ts +74 -0
  108. package/dist/chunk-FQCT3I2A.js.map +0 -1
  109. package/dist/chunk-GXY66V7Q.js.map +0 -1
  110. package/dist/chunk-U5QGN3RT.js.map +0 -1
  111. package/dist/chunk-XCF4PLN7.js.map +0 -1
  112. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -8,23 +8,27 @@
8
8
 
9
9
  import type {
10
10
  WorkflowDefinition,
11
- WorkflowStep,
12
- WorkflowContext,
13
11
  RunResult,
14
12
  Logger,
15
13
  SpawnChildOptions,
16
- } from "./types";
17
- import type { StorageAdapter } from "../storage/types";
18
- import type { EventTransport, WorkflowEvent } from "../events/types";
14
+ } from './types';
15
+ import type { StorageAdapter } from '../storage/types';
16
+ import type { EventTransport } from '../events/types';
17
+ import { WorkflowEngineError, WorkflowCanceledError } from '../utils/errors';
19
18
  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";
19
+ markRunRunning,
20
+ saveCheckpoint,
21
+ markRunSucceeded,
22
+ markRunFinal,
23
+ safeEmitEvent,
24
+ } from './orchestrator/persistence';
25
+ import { decideRunFinalState } from './orchestrator/state-transitions';
26
+ import { executeStep } from './orchestrator/step-runner';
27
+ import {
28
+ buildContext,
29
+ setupWorkflowTimeout,
30
+ initialCompletedSteps,
31
+ } from './orchestrator/plan-resolution';
28
32
 
29
33
  /**
30
34
  * Checkpoint data for resuming workflows.
@@ -66,9 +70,7 @@ export interface ExecuteOptions {
66
70
  * Execute a workflow definition.
67
71
  * This is the main execution logic that runs all steps sequentially.
68
72
  */
69
- export async function executeWorkflow(
70
- options: ExecuteOptions,
71
- ): Promise<RunResult> {
73
+ export async function executeWorkflow(options: ExecuteOptions): Promise<RunResult> {
72
74
  const {
73
75
  runId,
74
76
  definition,
@@ -87,56 +89,31 @@ export async function executeWorkflow(
87
89
 
88
90
  // Update run status to running (if not already)
89
91
  // On resume, don't overwrite the original startedAt
90
- await storage.updateRun(runId, {
91
- status: "running",
92
- ...(isResume ? {} : { startedAt: new Date() }),
93
- });
92
+ await markRunRunning(storage, runId, isResume);
94
93
 
95
- emitEvent(events, logger, {
94
+ safeEmitEvent(events, logger, {
96
95
  runId,
97
96
  kind: definition.kind,
98
- eventType: isResume ? "run.resumed" : "run.started",
97
+ eventType: isResume ? 'run.resumed' : 'run.started',
99
98
  timestamp: new Date(),
100
- payload: isResume
101
- ? { resumedFrom: Array.from(checkpoint.completedStepKeys) }
102
- : undefined,
99
+ payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : undefined,
103
100
  });
104
101
 
105
- // Build the execution context, using checkpoint results if resuming
106
- const context: WorkflowContext = {
102
+ const completedSteps = initialCompletedSteps(checkpoint);
103
+
104
+ const context = buildContext({
107
105
  runId,
108
- stepId: "", // Will be set for each step before handler is called
109
- kind: definition.kind,
106
+ definition,
110
107
  input,
111
- results: checkpoint?.results ? { ...checkpoint.results } : {},
112
108
  metadata,
113
- logger: createScopedLogger(logger, runId),
114
- signal: abortController.signal,
109
+ events,
110
+ logger,
111
+ abortController,
115
112
  spawnChild,
116
- emit: (eventType: string, payload?: unknown) => {
117
- emitEvent(events, logger, {
118
- runId,
119
- kind: definition.kind,
120
- eventType,
121
- timestamp: new Date(),
122
- payload,
123
- });
124
- },
125
- };
126
-
127
- // Set up workflow-level timeout if specified
128
- let workflowTimeoutId: ReturnType<typeof setTimeout> | undefined;
129
- let workflowTimeoutError: WorkflowTimeoutError | undefined;
130
-
131
- if (definition.timeout) {
132
- workflowTimeoutId = setTimeout(() => {
133
- workflowTimeoutError = new WorkflowTimeoutError(
134
- runId,
135
- definition.timeout!,
136
- );
137
- abortController.abort();
138
- }, definition.timeout);
139
- }
113
+ checkpointResults: checkpoint?.results,
114
+ });
115
+
116
+ const timeoutHandle = setupWorkflowTimeout(runId, definition, abortController);
140
117
 
141
118
  try {
142
119
  // Execute beforeRun hook
@@ -155,16 +132,14 @@ export async function executeWorkflow(
155
132
 
156
133
  // Skip already completed steps when resuming
157
134
  if (checkpoint?.completedStepKeys.has(step.key)) {
158
- logger.info(
159
- `Skipping step "${step.key}" (already completed in previous run)`,
160
- );
161
- emitEvent(events, logger, {
135
+ logger.info(`Skipping step "${step.key}" (already completed in previous run)`);
136
+ safeEmitEvent(events, logger, {
162
137
  runId,
163
138
  kind: definition.kind,
164
- eventType: "step.skipped",
139
+ eventType: 'step.skipped',
165
140
  stepKey: step.key,
166
141
  timestamp: new Date(),
167
- payload: { reason: "checkpoint" },
142
+ payload: { reason: 'checkpoint' },
168
143
  });
169
144
  continue;
170
145
  }
@@ -175,10 +150,10 @@ export async function executeWorkflow(
175
150
  if (shouldSkip) {
176
151
  logger.info(`Skipping step "${step.key}" (condition met)`);
177
152
 
178
- emitEvent(events, logger, {
153
+ safeEmitEvent(events, logger, {
179
154
  runId,
180
155
  kind: definition.kind,
181
- eventType: "step.skipped",
156
+ eventType: 'step.skipped',
182
157
  stepKey: step.key,
183
158
  timestamp: new Date(),
184
159
  });
@@ -199,21 +174,18 @@ export async function executeWorkflow(
199
174
  // Store result in context
200
175
  context.results[step.key] = result;
201
176
 
202
- // Save checkpoint (accumulated results)
203
- await storage.updateRun(runId, {
204
- context: { ...context.results },
205
- });
206
- }
177
+ // Track completed step
178
+ completedSteps.push(step.key);
207
179
 
208
- // All steps completed successfully
209
- // Clear workflow timeout
210
- if (workflowTimeoutId) {
211
- clearTimeout(workflowTimeoutId);
180
+ // Save checkpoint (accumulated results + completed steps)
181
+ await saveCheckpoint(storage, runId, context.results, completedSteps);
212
182
  }
213
183
 
184
+ timeoutHandle.clear();
185
+
214
186
  const duration = Date.now() - startTime;
215
187
  const runResult: RunResult = {
216
- status: "succeeded",
188
+ status: 'succeeded',
217
189
  results: context.results,
218
190
  duration,
219
191
  };
@@ -224,43 +196,28 @@ export async function executeWorkflow(
224
196
  }
225
197
 
226
198
  // Update run status
227
- await storage.updateRun(runId, {
228
- status: "succeeded",
229
- context: context.results,
230
- finishedAt: new Date(),
231
- });
199
+ await markRunSucceeded(storage, runId, context.results);
232
200
 
233
- emitEvent(events, logger, {
201
+ safeEmitEvent(events, logger, {
234
202
  runId,
235
203
  kind: definition.kind,
236
- eventType: "run.completed",
204
+ eventType: 'run.completed',
237
205
  timestamp: new Date(),
238
206
  payload: { results: context.results, duration },
239
207
  });
240
208
 
241
209
  return runResult;
210
+
242
211
  } catch (error) {
243
- // Clear workflow timeout
244
- if (workflowTimeoutId) {
245
- clearTimeout(workflowTimeoutId);
246
- }
212
+ timeoutHandle.clear();
247
213
 
248
214
  const duration = Date.now() - startTime;
249
215
 
250
- // Check if this was a workflow timeout (abort triggered by our timeout handler)
251
- const actualError = workflowTimeoutError ?? error;
216
+ const actualError = timeoutHandle.getTimeoutError() ?? error;
252
217
  const workflowError = WorkflowEngineError.fromError(actualError);
253
218
 
254
- // Determine final status
255
- 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";
219
+ // Determine final status + event type from the surfaced error.
220
+ const { status, eventType } = decideRunFinalState(actualError);
264
221
 
265
222
  const runResult: RunResult = {
266
223
  status,
@@ -274,19 +231,14 @@ export async function executeWorkflow(
274
231
  try {
275
232
  await definition.hooks.afterRun(context, runResult);
276
233
  } catch (hookError) {
277
- logger.error("afterRun hook failed:", hookError);
234
+ logger.error('afterRun hook failed for run ' + runId + ':', hookError);
278
235
  }
279
236
  }
280
237
 
281
238
  // Update run status
282
- await storage.updateRun(runId, {
283
- status,
284
- context: context.results,
285
- error: workflowError,
286
- finishedAt: new Date(),
287
- });
239
+ await markRunFinal(storage, runId, status, context.results, workflowError);
288
240
 
289
- emitEvent(events, logger, {
241
+ safeEmitEvent(events, logger, {
290
242
  runId,
291
243
  kind: definition.kind,
292
244
  eventType,
@@ -298,293 +250,3 @@ export async function executeWorkflow(
298
250
  }
299
251
  }
300
252
 
301
- /**
302
- * Options for executing a single step.
303
- */
304
- interface StepExecutionOptions<TInput = Record<string, unknown>> {
305
- definition: WorkflowDefinition<TInput>;
306
- storage: StorageAdapter;
307
- events: EventTransport;
308
- logger: Logger;
309
- abortController: AbortController;
310
- }
311
-
312
- /**
313
- * Execute a single workflow step with error handling and retries.
314
- */
315
- async function executeStep<TInput>(
316
- step: WorkflowStep<TInput>,
317
- context: WorkflowContext<TInput>,
318
- options: StepExecutionOptions<TInput>,
319
- ): Promise<unknown> {
320
- const { definition, storage, events, logger, abortController } = options;
321
-
322
- // Determine error strategy
323
- const onError = step.onError ?? definition.defaultOnError ?? "fail";
324
- const maxRetries = step.maxRetries ?? 3;
325
- const retryDelay = step.retryDelay ?? 1000;
326
- const retryBackoff = step.retryBackoff ?? 2;
327
-
328
- let attempt = 0;
329
- let lastError: Error | undefined;
330
-
331
- while (true) {
332
- attempt++;
333
-
334
- // Check for cancellation
335
- if (abortController.signal.aborted) {
336
- throw new WorkflowCanceledError(context.runId);
337
- }
338
-
339
- // Create step record
340
- const stepRecord = await storage.createStep({
341
- runId: context.runId,
342
- stepKey: step.key,
343
- stepName: step.name,
344
- status: "running",
345
- attempt,
346
- startedAt: new Date(),
347
- });
348
-
349
- // Set the current step's ID in context so handlers can track granular operations (e.g., token usage)
350
- context.stepId = stepRecord.id;
351
-
352
- // Emit step started event
353
- emitEvent(events, logger, {
354
- runId: context.runId,
355
- kind: context.kind,
356
- eventType: "step.started",
357
- stepKey: step.key,
358
- timestamp: new Date(),
359
- payload: { attempt },
360
- });
361
-
362
- // Execute beforeStep hook
363
- if (definition.hooks?.beforeStep) {
364
- await definition.hooks.beforeStep(context, step);
365
- }
366
-
367
- try {
368
- // Execute with optional timeout
369
- // Always race against abort signal to support workflow-level timeouts
370
- let result: unknown;
371
-
372
- if (step.timeout) {
373
- result = await executeWithTimeout(
374
- () => step.handler(context),
375
- step.timeout,
376
- abortController.signal,
377
- step.key,
378
- );
379
- } else {
380
- // Race step handler against abort signal for workflow-level timeout support
381
- result = await raceWithAbort(
382
- step.handler(context),
383
- abortController.signal,
384
- );
385
- }
386
-
387
- // Step succeeded
388
- await storage.updateStep(stepRecord.id, {
389
- status: "succeeded",
390
- result,
391
- finishedAt: new Date(),
392
- });
393
-
394
- // Execute afterStep hook
395
- if (definition.hooks?.afterStep) {
396
- await definition.hooks.afterStep(context, step, result);
397
- }
398
-
399
- emitEvent(events, logger, {
400
- runId: context.runId,
401
- kind: context.kind,
402
- eventType: "step.completed",
403
- stepKey: step.key,
404
- timestamp: new Date(),
405
- payload: { result },
406
- });
407
-
408
- return result;
409
- } catch (error) {
410
- lastError = error instanceof Error ? error : new Error(String(error));
411
-
412
- // Check if this is a cancellation (abort signal fired)
413
- const isCanceled =
414
- error instanceof WorkflowCanceledError ||
415
- abortController.signal.aborted;
416
-
417
- // Update step record with error
418
- await storage.updateStep(stepRecord.id, {
419
- status: isCanceled ? "canceled" : "failed",
420
- error: WorkflowEngineError.fromError(error),
421
- finishedAt: new Date(),
422
- });
423
-
424
- // If canceled, re-throw immediately without retry/skip logic
425
- if (isCanceled) {
426
- emitEvent(events, logger, {
427
- runId: context.runId,
428
- kind: context.kind,
429
- eventType: "step.failed",
430
- stepKey: step.key,
431
- timestamp: new Date(),
432
- payload: { error: lastError.message, attempt },
433
- });
434
- throw error;
435
- }
436
-
437
- // Execute onStepError hook
438
- if (definition.hooks?.onStepError) {
439
- try {
440
- await definition.hooks.onStepError(context, step, lastError);
441
- } catch (hookError) {
442
- logger.error("onStepError hook failed:", hookError);
443
- }
444
- }
445
-
446
- emitEvent(events, logger, {
447
- runId: context.runId,
448
- kind: context.kind,
449
- eventType: "step.failed",
450
- stepKey: step.key,
451
- timestamp: new Date(),
452
- payload: { error: lastError.message, attempt },
453
- });
454
-
455
- // Handle error based on strategy
456
- if (onError === "skip") {
457
- logger.warn(`Step "${step.key}" failed, skipping (strategy: skip)`);
458
- emitEvent(events, logger, {
459
- runId: context.runId,
460
- kind: context.kind,
461
- eventType: "step.skipped",
462
- stepKey: step.key,
463
- timestamp: new Date(),
464
- payload: { reason: "error", error: lastError.message },
465
- });
466
- return undefined;
467
- }
468
-
469
- if (onError === "retry" && attempt <= maxRetries) {
470
- const delay = calculateRetryDelay(attempt, retryDelay, retryBackoff);
471
- logger.warn(
472
- `Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`,
473
- );
474
-
475
- emitEvent(events, logger, {
476
- runId: context.runId,
477
- kind: context.kind,
478
- eventType: "step.retry",
479
- stepKey: step.key,
480
- timestamp: new Date(),
481
- payload: { attempt, maxRetries, delay, error: lastError.message },
482
- });
483
-
484
- try {
485
- await sleep(delay, abortController.signal);
486
- } catch (sleepError) {
487
- if (sleepError instanceof WorkflowCanceledError) {
488
- await storage.updateStep(stepRecord.id, {
489
- status: "canceled",
490
- finishedAt: new Date(),
491
- });
492
- }
493
- throw sleepError;
494
- }
495
- continue;
496
- }
497
-
498
- // Strategy is 'fail' or retries exhausted
499
- throw new StepError(step.key, lastError.message, attempt, lastError);
500
- }
501
- }
502
- }
503
-
504
- /**
505
- * Execute a function with a timeout.
506
- *
507
- * Uses Promise.race to resolve as soon as either the function completes, the
508
- * timeout fires, or the abort signal triggers. The `finally` block is critical:
509
- * without it, the losing promise's timer/listener would remain active, leaking
510
- * memory — especially problematic in long-running engines processing many steps.
511
- */
512
- async function executeWithTimeout<T>(
513
- fn: () => Promise<T>,
514
- timeoutMs: number,
515
- signal: AbortSignal,
516
- stepKey: string,
517
- ): Promise<T> {
518
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
519
- let onAbort: (() => void) | undefined;
520
-
521
- try {
522
- return await Promise.race([
523
- fn(),
524
- new Promise<never>((_, reject) => {
525
- timeoutId = setTimeout(() => {
526
- reject(new StepTimeoutError(stepKey, timeoutMs));
527
- }, timeoutMs);
528
-
529
- onAbort = () => {
530
- clearTimeout(timeoutId);
531
- reject(new WorkflowCanceledError("run"));
532
- };
533
- signal.addEventListener("abort", onAbort, { once: true });
534
- }),
535
- ]);
536
- } finally {
537
- if (timeoutId) clearTimeout(timeoutId);
538
- if (onAbort) signal.removeEventListener("abort", onAbort);
539
- }
540
- }
541
-
542
- /**
543
- * Race a promise against an abort signal.
544
- *
545
- * Steps without their own timeout still need to respect workflow-level timeouts
546
- * and cancellation. This wraps the step handler in a Promise.race so that an
547
- * abort signal (from workflow timeout or user cancellation) can interrupt it.
548
- *
549
- * The `finally` cleanup removes the abort listener to prevent accumulating
550
- * listeners on the signal across many steps — without it, each step would
551
- * leave a dangling listener even after completing successfully.
552
- */
553
- async function raceWithAbort<T>(
554
- promise: Promise<T>,
555
- signal: AbortSignal,
556
- ): Promise<T> {
557
- if (signal.aborted) {
558
- throw new WorkflowCanceledError("run");
559
- }
560
-
561
- let onAbort: (() => void) | undefined;
562
-
563
- try {
564
- return await Promise.race([
565
- promise,
566
- new Promise<never>((_, reject) => {
567
- onAbort = () => reject(new WorkflowCanceledError("run"));
568
- signal.addEventListener("abort", onAbort, { once: true });
569
- }),
570
- ]);
571
- } finally {
572
- if (onAbort) signal.removeEventListener("abort", onAbort);
573
- }
574
- }
575
-
576
- /**
577
- * Helper to emit an event.
578
- */
579
- function emitEvent(
580
- events: EventTransport,
581
- logger: Logger,
582
- event: WorkflowEvent,
583
- ): void {
584
- try {
585
- events.emit(event);
586
- } catch (error) {
587
- // Log but don't fail the workflow for event emission errors
588
- logger.error("Failed to emit event:", error);
589
- }
590
- }
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';