@multiplier-labs/stepflow 0.3.4 → 0.3.6

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 (99) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-U3PCVK6O.js → chunk-D5HW7VEG.js} +4 -2
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-5XJBAPXF.js → chunk-JUPZEBR3.js} +3 -1
  5. package/dist/{chunk-5XJBAPXF.js.map → chunk-JUPZEBR3.js.map} +1 -1
  6. package/dist/events/index.d.ts +2 -0
  7. package/dist/events/index.js +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/storage/index.js +1 -1
  10. package/package.json +23 -12
  11. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  12. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  13. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  14. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  15. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  16. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  17. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  18. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  19. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  20. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  21. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  22. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  23. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  24. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  25. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  26. package/.github/CODEOWNERS +0 -15
  27. package/.github/dependabot.yml +0 -23
  28. package/.github/workflows/ci.yml +0 -38
  29. package/.github/workflows/publish.yml +0 -42
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-U3PCVK6O.js.map +0 -1
  34. package/docs/github-packages-setup.md +0 -97
  35. package/docs/stepflow-api-reference.md +0 -1663
  36. package/src/core/engine/concurrency.ts +0 -229
  37. package/src/core/engine/lifecycle.ts +0 -176
  38. package/src/core/engine.test.ts +0 -1435
  39. package/src/core/engine.ts +0 -598
  40. package/src/core/index.ts +0 -7
  41. package/src/core/orchestrator/persistence.ts +0 -165
  42. package/src/core/orchestrator/plan-resolution.ts +0 -140
  43. package/src/core/orchestrator/state-transitions.ts +0 -109
  44. package/src/core/orchestrator/step-runner.ts +0 -427
  45. package/src/core/orchestrator.ts +0 -252
  46. package/src/core/types.ts +0 -220
  47. package/src/events/index.ts +0 -8
  48. package/src/events/memory.test.ts +0 -107
  49. package/src/events/memory.ts +0 -92
  50. package/src/events/socketio.test.ts +0 -378
  51. package/src/events/socketio.ts +0 -262
  52. package/src/events/types.ts +0 -118
  53. package/src/events/webhook.test.ts +0 -949
  54. package/src/events/webhook.ts +0 -807
  55. package/src/index.ts +0 -270
  56. package/src/planning/evaluate-condition.test.ts +0 -153
  57. package/src/planning/evaluate-condition.ts +0 -105
  58. package/src/planning/index.ts +0 -55
  59. package/src/planning/planner.ts +0 -494
  60. package/src/planning/planning.test.ts +0 -727
  61. package/src/planning/registry.test.ts +0 -398
  62. package/src/planning/registry.ts +0 -283
  63. package/src/planning/types.ts +0 -566
  64. package/src/scheduler/cron.test.ts +0 -479
  65. package/src/scheduler/cron.ts +0 -438
  66. package/src/scheduler/index.ts +0 -8
  67. package/src/scheduler/postgres-persistence.ts +0 -530
  68. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  69. package/src/scheduler/sqlite-persistence.ts +0 -286
  70. package/src/scheduler/types.ts +0 -73
  71. package/src/storage/index.ts +0 -27
  72. package/src/storage/memory.test.ts +0 -349
  73. package/src/storage/memory.ts +0 -227
  74. package/src/storage/postgres-core.ts +0 -550
  75. package/src/storage/postgres-migration.test.ts +0 -69
  76. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  77. package/src/storage/postgres.ts +0 -835
  78. package/src/storage/sqlite-core.ts +0 -151
  79. package/src/storage/sqlite.test.ts +0 -642
  80. package/src/storage/sqlite.ts +0 -717
  81. package/src/storage/types.ts +0 -343
  82. package/src/utils/errors.test.ts +0 -150
  83. package/src/utils/errors.ts +0 -161
  84. package/src/utils/id.ts +0 -21
  85. package/src/utils/index.ts +0 -10
  86. package/src/utils/logger.test.ts +0 -121
  87. package/src/utils/logger.ts +0 -96
  88. package/src/utils/pinned-fetch.test.ts +0 -84
  89. package/src/utils/pinned-fetch.ts +0 -118
  90. package/src/utils/postgres-deps.ts +0 -79
  91. package/src/utils/retry.test.ts +0 -177
  92. package/src/utils/retry.ts +0 -121
  93. package/src/utils/safe-json.test.ts +0 -166
  94. package/src/utils/safe-json.ts +0 -107
  95. package/src/utils/webhook-signature.test.ts +0 -111
  96. package/src/utils/webhook-signature.ts +0 -74
  97. package/tsconfig.json +0 -21
  98. package/tsup.config.ts +0 -14
  99. package/vitest.config.ts +0 -9
@@ -1,229 +0,0 @@
1
- /**
2
- * Concurrency control for the WorkflowEngine.
3
- *
4
- * Owns:
5
- * - the priority-ordered queue of pending runs (`runQueue`)
6
- * - the set of in-flight run abort controllers (`activeRuns`)
7
- * - the timer handles used to defer launches via setTimeout/setImmediate
8
- *
9
- * Responsibilities:
10
- * - `hasCapacity()` against the configured `maxConcurrency` ceiling
11
- * - priority-ordered enqueue (higher priority first, FIFO within a priority)
12
- * - dispatch loop that drains the queue while capacity exists, with a
13
- * try/catch around each dispatch so a single bad run cannot tear down
14
- * the loop and strand the rest of the queue (preserves the audit
15
- * 2026-04-27 C3 fix in `processQueue`).
16
- * - dispatch-failure telemetry: log + emit `run.failed` + best-effort
17
- * storage update with `QUEUE_DISPATCH_FAILED`.
18
- *
19
- * NOT responsible for:
20
- * - building the `executeWorkflow` invocation (that lives in WorkflowEngine
21
- * so the orchestrator/spawnChild wiring stays where it is)
22
- * - storage initialization / shutdown sequencing (see `lifecycle.ts`)
23
- */
24
-
25
- import type { StorageAdapter } from '../../storage/types';
26
- import type { EventTransport } from '../../events/types';
27
- import type { Logger, WorkflowDefinition } from '../types';
28
-
29
- /**
30
- * Queued run waiting to be executed when capacity is available.
31
- */
32
- export interface QueuedRun {
33
- runId: string;
34
- definition: WorkflowDefinition;
35
- input: Record<string, unknown>;
36
- metadata?: Record<string, unknown>;
37
- priority: number;
38
- queuedAt: Date;
39
- }
40
-
41
- type TimerHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;
42
-
43
- export interface ConcurrencyManagerDeps {
44
- storage: StorageAdapter;
45
- events: EventTransport;
46
- logger: Logger;
47
- /** Maximum concurrent runs; undefined or <=0 means unlimited. */
48
- maxConcurrency?: number;
49
- }
50
-
51
- export class ConcurrencyManager {
52
- private readonly activeRuns = new Map<string, AbortController>();
53
- private readonly runQueue: QueuedRun[] = [];
54
- private readonly timerHandles = new Set<TimerHandle>();
55
- private readonly storage: StorageAdapter;
56
- private readonly events: EventTransport;
57
- private readonly logger: Logger;
58
- private readonly maxConcurrency: number | undefined;
59
-
60
- constructor(deps: ConcurrencyManagerDeps) {
61
- this.storage = deps.storage;
62
- this.events = deps.events;
63
- this.logger = deps.logger;
64
- this.maxConcurrency = deps.maxConcurrency;
65
- }
66
-
67
- hasCapacity(): boolean {
68
- if (this.maxConcurrency === undefined || this.maxConcurrency <= 0) {
69
- return true;
70
- }
71
- return this.activeRuns.size < this.maxConcurrency;
72
- }
73
-
74
- getActiveCount(): number {
75
- return this.activeRuns.size;
76
- }
77
-
78
- getQueuedCount(): number {
79
- return this.runQueue.length;
80
- }
81
-
82
- isActive(runId: string): boolean {
83
- return this.activeRuns.has(runId);
84
- }
85
-
86
- /** Insert a run in priority order (higher priority first, FIFO within same). */
87
- enqueue(run: QueuedRun): void {
88
- let inserted = false;
89
- for (let i = 0; i < this.runQueue.length; i++) {
90
- if (run.priority > this.runQueue[i].priority) {
91
- this.runQueue.splice(i, 0, run);
92
- inserted = true;
93
- break;
94
- }
95
- }
96
- if (!inserted) {
97
- this.runQueue.push(run);
98
- }
99
- }
100
-
101
- /** Remove a run from the queue if present. Returns true if it was queued. */
102
- removeFromQueue(runId: string): boolean {
103
- const idx = this.runQueue.findIndex((q) => q.runId === runId);
104
- if (idx === -1) return false;
105
- this.runQueue.splice(idx, 1);
106
- return true;
107
- }
108
-
109
- registerActive(runId: string, controller: AbortController): void {
110
- this.activeRuns.set(runId, controller);
111
- }
112
-
113
- unregisterActive(runId: string): void {
114
- this.activeRuns.delete(runId);
115
- }
116
-
117
- abortActive(runId: string): boolean {
118
- const controller = this.activeRuns.get(runId);
119
- if (!controller) return false;
120
- controller.abort();
121
- return true;
122
- }
123
-
124
- /**
125
- * Abort every in-flight run. `onAbort` is called for each run id before its
126
- * controller is signaled (so callers can log, emit, etc.).
127
- */
128
- abortAllActive(onAbort?: (runId: string) => void): void {
129
- for (const [runId, controller] of this.activeRuns) {
130
- if (onAbort) onAbort(runId);
131
- controller.abort();
132
- }
133
- }
134
-
135
- /** Forget all active-run bookkeeping without aborting (used during shutdown). */
136
- clearActive(): void {
137
- this.activeRuns.clear();
138
- }
139
-
140
- /**
141
- * Schedule a launch via setTimeout (when `delay` is truthy) or setImmediate
142
- * (otherwise). Tracks the handle so it can be cleared during shutdown.
143
- */
144
- scheduleLaunch(fn: () => void, delay?: number): void {
145
- if (delay) {
146
- const handle = setTimeout(fn, delay);
147
- this.timerHandles.add(handle);
148
- } else {
149
- const handle = setImmediate(fn);
150
- this.timerHandles.add(handle);
151
- }
152
- }
153
-
154
- clearTimers(): void {
155
- for (const handle of this.timerHandles) {
156
- if (typeof handle === 'object' && 'ref' in handle) {
157
- clearImmediate(handle as ReturnType<typeof setImmediate>);
158
- } else {
159
- clearTimeout(handle as ReturnType<typeof setTimeout>);
160
- }
161
- }
162
- this.timerHandles.clear();
163
- }
164
-
165
- /**
166
- * Drain the queue while capacity is available, dispatching each item via
167
- * `dispatch(run)`. Each iteration is wrapped in try/catch so that a failure
168
- * dispatching one queued item (e.g., an event-transport that throws) cannot
169
- * tear down the loop and leave remaining items stuck in the queue. Failed
170
- * dispatches are logged, the run is marked as failed in storage as
171
- * best-effort telemetry, and the loop advances to the next item.
172
- *
173
- * Emits `run.dequeued` for each dispatched run (this is what the C3
174
- * resilience test exercises by having the emit throw).
175
- */
176
- processQueue(dispatch: (run: QueuedRun) => void): void {
177
- while (this.hasCapacity() && this.runQueue.length > 0) {
178
- const next = this.runQueue.shift()!;
179
-
180
- try {
181
- this.logger.debug(`Starting queued run ${next.runId}`);
182
-
183
- this.events.emit({
184
- runId: next.runId,
185
- kind: next.definition.kind,
186
- eventType: 'run.dequeued',
187
- timestamp: new Date(),
188
- });
189
-
190
- dispatch(next);
191
- } catch (err) {
192
- this.recordQueueDispatchFailure(next, err);
193
- }
194
- }
195
- }
196
-
197
- /**
198
- * Record a queue-dispatch failure: log it, emit a failure event, and
199
- * best-effort mark the run as failed in storage. All side-effects are
200
- * defensively wrapped so this method itself never throws.
201
- */
202
- private recordQueueDispatchFailure(queued: QueuedRun, err: unknown): void {
203
- const message = err instanceof Error ? err.message : String(err);
204
- this.logger.error(`Failed to dispatch queued run ${queued.runId}`, err);
205
-
206
- try {
207
- this.events.emit({
208
- runId: queued.runId,
209
- kind: queued.definition.kind,
210
- eventType: 'run.failed',
211
- timestamp: new Date(),
212
- payload: { error: { code: 'QUEUE_DISPATCH_FAILED', message } },
213
- });
214
- } catch (emitErr) {
215
- this.logger.error(`Failed to emit dispatch-failure event for ${queued.runId}`, emitErr);
216
- }
217
-
218
- // Fire-and-forget; we don't want to block the queue loop on storage I/O.
219
- this.storage
220
- .updateRun(queued.runId, {
221
- status: 'failed',
222
- finishedAt: new Date(),
223
- error: { code: 'QUEUE_DISPATCH_FAILED', message },
224
- })
225
- .catch((updateErr) => {
226
- this.logger.error(`Failed to record dispatch failure for ${queued.runId}`, updateErr);
227
- });
228
- }
229
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Lifecycle and resume orchestration for the WorkflowEngine.
3
- *
4
- * Owns the cross-cutting "engine itself" lifecycle:
5
- * - `initialize()` — bring up storage adapters that need explicit init
6
- * (e.g., PostgresStorageAdapter)
7
- * - `shutdown()` — drain timers, abort in-flight runs, close transports
8
- * - `resumeRun()` / `getResumableRuns()` / `resumeAllInterrupted()` —
9
- * restart interrupted runs from their persisted checkpoints
10
- *
11
- * NOT responsible for:
12
- * - per-step execution (that lives in `orchestrator.ts`)
13
- * - queue/active/timer bookkeeping (that lives in `engine/concurrency.ts`;
14
- * this module delegates to a `ConcurrencyManager`)
15
- * - building the `executeWorkflow` invocation (the engine still owns that
16
- * so the orchestrator/spawnChild wiring stays in one place; this module
17
- * calls it via the injected `launchRun` callback)
18
- */
19
-
20
- import type { StorageAdapter, WorkflowRunRecord } from '../../storage/types';
21
- import type { EventTransport } from '../../events/types';
22
- import type { Logger, WorkflowDefinition, WorkflowKind } from '../types';
23
- import { RunNotFoundError, WorkflowNotFoundError } from '../../utils/errors';
24
- import type { ConcurrencyManager } from './concurrency';
25
-
26
- /**
27
- * Callback the engine provides for actually launching a run. Lifecycle uses
28
- * it to start a resumed run with its checkpoint; the engine's implementation
29
- * registers the abort controller, schedules the launch via the concurrency
30
- * manager, and invokes the orchestrator.
31
- */
32
- export type LaunchRunFn = (
33
- runId: string,
34
- definition: WorkflowDefinition,
35
- input: Record<string, unknown>,
36
- metadata?: Record<string, unknown>,
37
- delay?: number,
38
- checkpoint?: { completedStepKeys: Set<string>; results: Record<string, unknown> }
39
- ) => void;
40
-
41
- export interface LifecycleManagerDeps {
42
- storage: StorageAdapter;
43
- events: EventTransport;
44
- logger: Logger;
45
- registry: Map<WorkflowKind, WorkflowDefinition>;
46
- concurrency: ConcurrencyManager;
47
- launchRun: LaunchRunFn;
48
- }
49
-
50
- export class LifecycleManager {
51
- private readonly storage: StorageAdapter;
52
- private readonly events: EventTransport;
53
- private readonly logger: Logger;
54
- private readonly registry: Map<WorkflowKind, WorkflowDefinition>;
55
- private readonly concurrency: ConcurrencyManager;
56
- private readonly launchRun: LaunchRunFn;
57
-
58
- constructor(deps: LifecycleManagerDeps) {
59
- this.storage = deps.storage;
60
- this.events = deps.events;
61
- this.logger = deps.logger;
62
- this.registry = deps.registry;
63
- this.concurrency = deps.concurrency;
64
- this.launchRun = deps.launchRun;
65
- }
66
-
67
- /**
68
- * Initialize the engine's storage adapter if it requires it.
69
- * Safe to call when the adapter has no `initialize` hook.
70
- */
71
- async initialize(): Promise<void> {
72
- if (this.storage.initialize) {
73
- await this.storage.initialize();
74
- }
75
- }
76
-
77
- /**
78
- * Shutdown the engine gracefully:
79
- * 1. clear pending timer handles (so no further launches fire)
80
- * 2. abort all in-flight runs
81
- * 3. close the event transport
82
- * 4. close the storage adapter
83
- * 5. drop active-run bookkeeping
84
- */
85
- async shutdown(): Promise<void> {
86
- this.logger.info('Shutting down workflow engine...');
87
-
88
- this.concurrency.clearTimers();
89
-
90
- this.concurrency.abortAllActive((runId) => {
91
- this.logger.debug(`Canceling active run: ${runId}`);
92
- });
93
-
94
- if (this.events.close) {
95
- this.events.close();
96
- }
97
-
98
- if (this.storage.close) {
99
- await this.storage.close();
100
- }
101
-
102
- this.concurrency.clearActive();
103
- this.logger.info('Workflow engine shutdown complete');
104
- }
105
-
106
- /**
107
- * Resume an interrupted workflow run from its checkpoint.
108
- * The run must be in 'queued' or 'running' status.
109
- */
110
- async resumeRun(runId: string): Promise<string> {
111
- const run = await this.storage.getRun(runId);
112
- if (!run) {
113
- throw new RunNotFoundError(runId);
114
- }
115
-
116
- if (!['queued', 'running'].includes(run.status)) {
117
- throw new Error(`Cannot resume run ${runId}: status is "${run.status}"`);
118
- }
119
-
120
- const definition = this.registry.get(run.kind);
121
- if (!definition) {
122
- throw new WorkflowNotFoundError(run.kind);
123
- }
124
-
125
- if (this.concurrency.isActive(runId)) {
126
- this.logger.warn(`Run ${runId} is already active, skipping resume`);
127
- return runId;
128
- }
129
-
130
- this.logger.info(`Resuming run ${runId} from checkpoint`);
131
-
132
- const completedStepKeys = new Set(run.completedSteps ?? []);
133
-
134
- this.launchRun(runId, definition, run.input, run.metadata, undefined, {
135
- completedStepKeys,
136
- results: run.context,
137
- });
138
-
139
- return runId;
140
- }
141
-
142
- /**
143
- * Get all runs that were interrupted and can be resumed.
144
- * Returns runs with status 'queued' or 'running'.
145
- */
146
- async getResumableRuns(): Promise<WorkflowRunRecord[]> {
147
- const result = await this.storage.listRuns({
148
- status: ['queued', 'running'],
149
- });
150
- return result.items;
151
- }
152
-
153
- /**
154
- * Resume every interrupted run whose workflow kind is currently registered.
155
- * Useful for recovering after a server restart.
156
- */
157
- async resumeAllInterrupted(): Promise<string[]> {
158
- const resumableRuns = await this.getResumableRuns();
159
- const resumedIds: string[] = [];
160
-
161
- for (const run of resumableRuns) {
162
- if (this.registry.has(run.kind)) {
163
- try {
164
- await this.resumeRun(run.id);
165
- resumedIds.push(run.id);
166
- } catch (error) {
167
- this.logger.error(`Failed to resume run ${run.id}:`, error);
168
- }
169
- } else {
170
- this.logger.warn(`Cannot resume run ${run.id}: workflow "${run.kind}" not registered`);
171
- }
172
- }
173
-
174
- return resumedIds;
175
- }
176
- }