@multiplier-labs/stepflow 0.3.4 → 0.3.8

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 (109) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-5XJBAPXF.js → chunk-4IDPWWGS.js} +15 -4
  3. package/dist/chunk-4IDPWWGS.js.map +1 -0
  4. package/dist/{chunk-U3PCVK6O.js → chunk-T6GAZBTQ.js} +70 -38
  5. package/dist/chunk-T6GAZBTQ.js.map +1 -0
  6. package/dist/{chunk-FMPJBK2N.js → chunk-TVSADUYU.js} +3 -5
  7. package/dist/{chunk-FMPJBK2N.js.map → chunk-TVSADUYU.js.map} +1 -1
  8. package/dist/events/index.d.ts +43 -3
  9. package/dist/events/index.js +7 -3
  10. package/dist/{index-Cd0IZ40L.d.ts → index-CdXDv30T.d.ts} +10 -2
  11. package/dist/index.d.ts +5 -4
  12. package/dist/index.js +159 -46
  13. package/dist/index.js.map +1 -1
  14. package/dist/scheduler/index.d.ts +2 -2
  15. package/dist/scheduler/index.js +1 -1
  16. package/dist/storage/index.d.ts +6 -4
  17. package/dist/storage/index.js +1 -1
  18. package/dist/{types-DSEjiLMj.d.ts → types-BfWe5yP6.d.ts} +2 -0
  19. package/package.json +26 -14
  20. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  21. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  22. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  23. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  24. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  25. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  26. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  27. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  28. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  29. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  30. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  31. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  32. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  33. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  34. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  35. package/.github/CODEOWNERS +0 -15
  36. package/.github/dependabot.yml +0 -23
  37. package/.github/workflows/ci.yml +0 -38
  38. package/.github/workflows/publish.yml +0 -42
  39. package/.github/workflows/security.yml +0 -29
  40. package/CHANGELOG.md +0 -34
  41. package/SECURITY.md +0 -40
  42. package/dist/chunk-5XJBAPXF.js.map +0 -1
  43. package/dist/chunk-U3PCVK6O.js.map +0 -1
  44. package/docs/github-packages-setup.md +0 -97
  45. package/docs/stepflow-api-reference.md +0 -1663
  46. package/src/core/engine/concurrency.ts +0 -229
  47. package/src/core/engine/lifecycle.ts +0 -176
  48. package/src/core/engine.test.ts +0 -1435
  49. package/src/core/engine.ts +0 -598
  50. package/src/core/index.ts +0 -7
  51. package/src/core/orchestrator/persistence.ts +0 -165
  52. package/src/core/orchestrator/plan-resolution.ts +0 -140
  53. package/src/core/orchestrator/state-transitions.ts +0 -109
  54. package/src/core/orchestrator/step-runner.ts +0 -427
  55. package/src/core/orchestrator.ts +0 -252
  56. package/src/core/types.ts +0 -220
  57. package/src/events/index.ts +0 -8
  58. package/src/events/memory.test.ts +0 -107
  59. package/src/events/memory.ts +0 -92
  60. package/src/events/socketio.test.ts +0 -378
  61. package/src/events/socketio.ts +0 -262
  62. package/src/events/types.ts +0 -118
  63. package/src/events/webhook.test.ts +0 -949
  64. package/src/events/webhook.ts +0 -807
  65. package/src/index.ts +0 -270
  66. package/src/planning/evaluate-condition.test.ts +0 -153
  67. package/src/planning/evaluate-condition.ts +0 -105
  68. package/src/planning/index.ts +0 -55
  69. package/src/planning/planner.ts +0 -494
  70. package/src/planning/planning.test.ts +0 -727
  71. package/src/planning/registry.test.ts +0 -398
  72. package/src/planning/registry.ts +0 -283
  73. package/src/planning/types.ts +0 -566
  74. package/src/scheduler/cron.test.ts +0 -479
  75. package/src/scheduler/cron.ts +0 -438
  76. package/src/scheduler/index.ts +0 -8
  77. package/src/scheduler/postgres-persistence.ts +0 -530
  78. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  79. package/src/scheduler/sqlite-persistence.ts +0 -286
  80. package/src/scheduler/types.ts +0 -73
  81. package/src/storage/index.ts +0 -27
  82. package/src/storage/memory.test.ts +0 -349
  83. package/src/storage/memory.ts +0 -227
  84. package/src/storage/postgres-core.ts +0 -550
  85. package/src/storage/postgres-migration.test.ts +0 -69
  86. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  87. package/src/storage/postgres.ts +0 -835
  88. package/src/storage/sqlite-core.ts +0 -151
  89. package/src/storage/sqlite.test.ts +0 -642
  90. package/src/storage/sqlite.ts +0 -717
  91. package/src/storage/types.ts +0 -343
  92. package/src/utils/errors.test.ts +0 -150
  93. package/src/utils/errors.ts +0 -161
  94. package/src/utils/id.ts +0 -21
  95. package/src/utils/index.ts +0 -10
  96. package/src/utils/logger.test.ts +0 -121
  97. package/src/utils/logger.ts +0 -96
  98. package/src/utils/pinned-fetch.test.ts +0 -84
  99. package/src/utils/pinned-fetch.ts +0 -118
  100. package/src/utils/postgres-deps.ts +0 -79
  101. package/src/utils/retry.test.ts +0 -177
  102. package/src/utils/retry.ts +0 -121
  103. package/src/utils/safe-json.test.ts +0 -166
  104. package/src/utils/safe-json.ts +0 -107
  105. package/src/utils/webhook-signature.test.ts +0 -111
  106. package/src/utils/webhook-signature.ts +0 -74
  107. package/tsconfig.json +0 -21
  108. package/tsup.config.ts +0 -14
  109. 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
- }