@multiplier-labs/stepflow 0.3.1

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 (76) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/README.md +408 -0
  3. package/dist/chunk-FQCT3I2A.js +1512 -0
  4. package/dist/chunk-FQCT3I2A.js.map +1 -0
  5. package/dist/chunk-GXY66V7Q.js +111 -0
  6. package/dist/chunk-GXY66V7Q.js.map +1 -0
  7. package/dist/chunk-U5QGN3RT.js +715 -0
  8. package/dist/chunk-U5QGN3RT.js.map +1 -0
  9. package/dist/chunk-XCF4PLN7.js +575 -0
  10. package/dist/chunk-XCF4PLN7.js.map +1 -0
  11. package/dist/events/index.d.ts +346 -0
  12. package/dist/events/index.js +11 -0
  13. package/dist/events/index.js.map +1 -0
  14. package/dist/index-CPfGsmrp.d.ts +567 -0
  15. package/dist/index.d.ts +748 -0
  16. package/dist/index.js +1707 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/scheduler/index.d.ts +6 -0
  19. package/dist/scheduler/index.js +12 -0
  20. package/dist/scheduler/index.js.map +1 -0
  21. package/dist/storage/index.d.ts +402 -0
  22. package/dist/storage/index.js +13 -0
  23. package/dist/storage/index.js.map +1 -0
  24. package/dist/types-BVShq7zT.d.ts +150 -0
  25. package/dist/types-BlwFEGzj.d.ts +276 -0
  26. package/dist/types-DzkEqnQJ.d.ts +75 -0
  27. package/docs/github-packages-setup.md +97 -0
  28. package/docs/stepflow-api-reference.md +1662 -0
  29. package/package.json +85 -0
  30. package/src/core/engine.test.ts +1421 -0
  31. package/src/core/engine.ts +717 -0
  32. package/src/core/index.ts +7 -0
  33. package/src/core/orchestrator.ts +590 -0
  34. package/src/core/types.ts +242 -0
  35. package/src/events/index.ts +8 -0
  36. package/src/events/memory.test.ts +109 -0
  37. package/src/events/memory.ts +95 -0
  38. package/src/events/socketio.test.ts +428 -0
  39. package/src/events/socketio.ts +269 -0
  40. package/src/events/types.ts +121 -0
  41. package/src/events/webhook.test.ts +695 -0
  42. package/src/events/webhook.ts +591 -0
  43. package/src/index.ts +270 -0
  44. package/src/planning/index.ts +52 -0
  45. package/src/planning/planner.ts +601 -0
  46. package/src/planning/planning.test.ts +790 -0
  47. package/src/planning/registry.test.ts +445 -0
  48. package/src/planning/registry.ts +369 -0
  49. package/src/planning/types.ts +570 -0
  50. package/src/scheduler/cron.test.ts +480 -0
  51. package/src/scheduler/cron.ts +449 -0
  52. package/src/scheduler/index.ts +18 -0
  53. package/src/scheduler/postgres-persistence.test.ts +906 -0
  54. package/src/scheduler/postgres-persistence.ts +546 -0
  55. package/src/scheduler/sqlite-persistence.test.ts +235 -0
  56. package/src/scheduler/sqlite-persistence.ts +268 -0
  57. package/src/scheduler/types.ts +78 -0
  58. package/src/storage/index.ts +27 -0
  59. package/src/storage/memory.test.ts +399 -0
  60. package/src/storage/memory.ts +248 -0
  61. package/src/storage/postgres.ts +1550 -0
  62. package/src/storage/sqlite.test.ts +593 -0
  63. package/src/storage/sqlite.ts +662 -0
  64. package/src/storage/types.ts +368 -0
  65. package/src/utils/errors.test.ts +152 -0
  66. package/src/utils/errors.ts +191 -0
  67. package/src/utils/id.ts +21 -0
  68. package/src/utils/index.ts +8 -0
  69. package/src/utils/logger.test.ts +127 -0
  70. package/src/utils/logger.ts +96 -0
  71. package/src/utils/postgres-deps.ts +63 -0
  72. package/src/utils/retry.test.ts +197 -0
  73. package/src/utils/retry.ts +121 -0
  74. package/tsconfig.json +21 -0
  75. package/tsup.config.ts +14 -0
  76. package/vitest.config.ts +9 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Core types for the workflow engine.
3
+ * These are the foundational interfaces used throughout the system.
4
+ */
5
+
6
+ // ============================================================================
7
+ // Basic Types
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Unique identifier for a workflow type.
12
+ * Used to register and look up workflow definitions.
13
+ */
14
+ export type WorkflowKind = string;
15
+
16
+ /**
17
+ * Status of a workflow run.
18
+ */
19
+ export type RunStatus =
20
+ | "queued"
21
+ | "running"
22
+ | "succeeded"
23
+ | "failed"
24
+ | "canceled";
25
+
26
+ /**
27
+ * Status of a workflow step.
28
+ */
29
+ export type StepStatus =
30
+ | "pending"
31
+ | "running"
32
+ | "succeeded"
33
+ | "failed"
34
+ | "skipped"
35
+ | "canceled";
36
+
37
+ /**
38
+ * Error handling strategy for a step.
39
+ * - 'fail': Stop the workflow immediately
40
+ * - 'retry': Retry the step up to maxRetries times
41
+ * - 'skip': Mark as skipped and continue to next step
42
+ */
43
+ export type StepErrorStrategy = "fail" | "retry" | "skip";
44
+
45
+ // ============================================================================
46
+ // Error Types
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Structured error information stored with runs and steps.
51
+ */
52
+ export interface WorkflowError {
53
+ code: string;
54
+ message: string;
55
+ stack?: string;
56
+ details?: Record<string, unknown>;
57
+ }
58
+
59
+ /**
60
+ * Result of a completed workflow run.
61
+ */
62
+ export interface RunResult {
63
+ status: "succeeded" | "failed" | "canceled";
64
+ results: Record<string, unknown>;
65
+ error?: WorkflowError;
66
+ duration: number;
67
+ }
68
+
69
+ // ============================================================================
70
+ // Logger Interface
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Minimal logger interface that consumers can implement.
75
+ * Compatible with console, pino, winston, etc.
76
+ */
77
+ export interface Logger {
78
+ debug(message: string, ...args: unknown[]): void;
79
+ info(message: string, ...args: unknown[]): void;
80
+ warn(message: string, ...args: unknown[]): void;
81
+ error(message: string, ...args: unknown[]): void;
82
+ }
83
+
84
+ // ============================================================================
85
+ // Spawn Child Options
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Options for spawning a child workflow from within a step handler.
90
+ */
91
+ export interface SpawnChildOptions {
92
+ kind: WorkflowKind;
93
+ input?: Record<string, unknown>;
94
+ metadata?: Record<string, unknown>;
95
+ }
96
+
97
+ // ============================================================================
98
+ // Workflow Context
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Runtime context passed to each step handler.
103
+ * Contains all information a step needs to execute and interact with the engine.
104
+ */
105
+ export interface WorkflowContext<TInput = Record<string, unknown>> {
106
+ /** Unique run identifier */
107
+ runId: string;
108
+
109
+ /** Current step identifier (for granular tracking, e.g., token usage) */
110
+ stepId: string;
111
+
112
+ /** Workflow type */
113
+ kind: WorkflowKind;
114
+
115
+ /** Input parameters passed when starting the run */
116
+ input: TInput;
117
+
118
+ /** Accumulated results from previous steps (keyed by step key) */
119
+ results: Record<string, unknown>;
120
+
121
+ /** Optional metadata (topicId, userId, etc.) */
122
+ metadata: Record<string, unknown>;
123
+
124
+ /** Current step key (set during execution) */
125
+ currentStep?: string;
126
+
127
+ /** Logger instance scoped to this run */
128
+ logger: Logger;
129
+
130
+ /** Abort signal for cancellation */
131
+ signal: AbortSignal;
132
+
133
+ /** Helper to spawn child workflows */
134
+ spawnChild: (options: SpawnChildOptions) => Promise<string>;
135
+
136
+ /** Helper to emit custom events */
137
+ emit: (eventType: string, payload?: unknown) => void;
138
+ }
139
+
140
+ // ============================================================================
141
+ // Workflow Step
142
+ // ============================================================================
143
+
144
+ /**
145
+ * Definition of a single workflow step.
146
+ * Steps are executed sequentially within a workflow.
147
+ */
148
+ export interface WorkflowStep<TInput = Record<string, unknown>> {
149
+ /** Unique step identifier within the workflow */
150
+ key: string;
151
+
152
+ /** Human-readable step name */
153
+ name: string;
154
+
155
+ /** Async function that performs the step's work */
156
+ handler: (ctx: WorkflowContext<TInput>) => Promise<unknown>;
157
+
158
+ /** What to do when the step fails (default: 'fail') */
159
+ onError?: StepErrorStrategy;
160
+
161
+ /** Maximum retry attempts when onError is 'retry' @default 3 */
162
+ maxRetries?: number;
163
+
164
+ /** Delay between retries in ms @default 1000 */
165
+ retryDelay?: number;
166
+
167
+ /** Exponential backoff multiplier @default 2 */
168
+ retryBackoff?: number;
169
+
170
+ /** Timeout for this step in ms (optional) */
171
+ timeout?: number;
172
+
173
+ /** Condition to skip this step */
174
+ skipIf?: (ctx: WorkflowContext<TInput>) => boolean | Promise<boolean>;
175
+ }
176
+
177
+ // ============================================================================
178
+ // Workflow Hooks
179
+ // ============================================================================
180
+
181
+ /**
182
+ * Lifecycle hooks for workflows.
183
+ * Allow custom logic at key points in the workflow execution.
184
+ */
185
+ export interface WorkflowHooks<TInput = Record<string, unknown>> {
186
+ /** Called before workflow starts */
187
+ beforeRun?: (ctx: WorkflowContext<TInput>) => Promise<void>;
188
+
189
+ /** Called after workflow completes (success or failure) */
190
+ afterRun?: (ctx: WorkflowContext<TInput>, result: RunResult) => Promise<void>;
191
+
192
+ /** Called before each step */
193
+ beforeStep?: (
194
+ ctx: WorkflowContext<TInput>,
195
+ step: WorkflowStep<TInput>,
196
+ ) => Promise<void>;
197
+
198
+ /** Called after each step (success only) */
199
+ afterStep?: (
200
+ ctx: WorkflowContext<TInput>,
201
+ step: WorkflowStep<TInput>,
202
+ result: unknown,
203
+ ) => Promise<void>;
204
+
205
+ /** Called when a step fails */
206
+ onStepError?: (
207
+ ctx: WorkflowContext<TInput>,
208
+ step: WorkflowStep<TInput>,
209
+ error: Error,
210
+ ) => Promise<void>;
211
+ }
212
+
213
+ // ============================================================================
214
+ // Workflow Definition
215
+ // ============================================================================
216
+
217
+ /**
218
+ * Complete workflow definition.
219
+ * Registered with the engine to enable starting runs of this type.
220
+ */
221
+ export interface WorkflowDefinition<TInput = Record<string, unknown>> {
222
+ /** Unique workflow type identifier */
223
+ kind: WorkflowKind;
224
+
225
+ /** Human-readable workflow name */
226
+ name: string;
227
+
228
+ /** Optional description */
229
+ description?: string;
230
+
231
+ /** Ordered list of steps to execute */
232
+ steps: WorkflowStep<TInput>[];
233
+
234
+ /** Default error strategy for all steps (default: 'fail') */
235
+ defaultOnError?: StepErrorStrategy;
236
+
237
+ /** Global timeout for the entire workflow in ms */
238
+ timeout?: number;
239
+
240
+ /** Hooks for workflow lifecycle */
241
+ hooks?: WorkflowHooks<TInput>;
242
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Events module exports.
3
+ */
4
+
5
+ export * from "./types";
6
+ export * from "./memory";
7
+ export * from "./socketio";
8
+ export * from "./webhook";
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { MemoryEventTransport } from "./memory";
3
+ import type { WorkflowEvent, WorkflowEventType } from "./types";
4
+
5
+ function createTestEvent(
6
+ overrides: Partial<WorkflowEvent> = {},
7
+ ): WorkflowEvent {
8
+ return {
9
+ eventType: "run.started",
10
+ runId: "run-123",
11
+ kind: "test.workflow",
12
+ timestamp: new Date("2024-01-01T12:00:00Z"),
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe("MemoryEventTransport", () => {
18
+ let transport: MemoryEventTransport;
19
+
20
+ beforeEach(() => {
21
+ transport = new MemoryEventTransport();
22
+ });
23
+
24
+ describe("subscribeToType", () => {
25
+ it("should call callback when event of matching type is emitted", () => {
26
+ const callback = vi.fn();
27
+ transport.subscribeToType("run.completed", callback);
28
+
29
+ const event = createTestEvent({ eventType: "run.completed" });
30
+ transport.emit(event);
31
+
32
+ expect(callback).toHaveBeenCalledWith(event);
33
+ });
34
+
35
+ it("should not call callback for events of a different type", () => {
36
+ const callback = vi.fn();
37
+ transport.subscribeToType("run.completed", callback);
38
+
39
+ transport.emit(createTestEvent({ eventType: "run.started" }));
40
+
41
+ expect(callback).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it("should unsubscribe when calling the returned function", () => {
45
+ const callback = vi.fn();
46
+ const unsubscribe = transport.subscribeToType("run.completed", callback);
47
+
48
+ // Emit once before unsubscribe
49
+ transport.emit(createTestEvent({ eventType: "run.completed" }));
50
+ expect(callback).toHaveBeenCalledTimes(1);
51
+
52
+ unsubscribe();
53
+
54
+ // Emit again after unsubscribe
55
+ transport.emit(createTestEvent({ eventType: "run.completed" }));
56
+ expect(callback).toHaveBeenCalledTimes(1);
57
+ });
58
+
59
+ it("should support multiple type subscriptions independently", () => {
60
+ const completedCb = vi.fn();
61
+ const failedCb = vi.fn();
62
+ transport.subscribeToType("run.completed", completedCb);
63
+ transport.subscribeToType("run.failed", failedCb);
64
+
65
+ transport.emit(createTestEvent({ eventType: "run.completed" }));
66
+
67
+ expect(completedCb).toHaveBeenCalledTimes(1);
68
+ expect(failedCb).not.toHaveBeenCalled();
69
+ });
70
+ });
71
+
72
+ describe("getListenerCount", () => {
73
+ it("should return 0 when no listeners are registered", () => {
74
+ expect(transport.getListenerCount()).toBe(0);
75
+ });
76
+
77
+ it("should return global listener count when called without channel", () => {
78
+ transport.subscribeAll(vi.fn());
79
+ transport.subscribeAll(vi.fn());
80
+
81
+ expect(transport.getListenerCount()).toBe(2);
82
+ });
83
+
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());
88
+
89
+ expect(transport.getListenerCount("run:run-1")).toBe(2);
90
+ expect(transport.getListenerCount("run:run-2")).toBe(1);
91
+ });
92
+
93
+ it("should update count after subscribe and unsubscribe", () => {
94
+ const unsub = transport.subscribeAll(vi.fn());
95
+ expect(transport.getListenerCount()).toBe(1);
96
+
97
+ unsub();
98
+ expect(transport.getListenerCount()).toBe(0);
99
+ });
100
+
101
+ it("should return count for type-based channels", () => {
102
+ transport.subscribeToType("step.completed", vi.fn());
103
+ transport.subscribeToType("step.completed", vi.fn());
104
+
105
+ expect(transport.getListenerCount("type:step.completed")).toBe(2);
106
+ expect(transport.getListenerCount("type:step.failed")).toBe(0);
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * In-memory event transport using Node.js EventEmitter.
3
+ * Default transport for development and single-process deployments.
4
+ *
5
+ * NOTE: This adapter is intended for development and testing only. It stores
6
+ * all state in-process with no persistence or multi-process safety guarantees.
7
+ */
8
+
9
+ import { EventEmitter } from "events";
10
+ import type {
11
+ EventTransport,
12
+ WorkflowEvent,
13
+ WorkflowEventType,
14
+ EventCallback,
15
+ Unsubscribe,
16
+ } from "./types";
17
+
18
+ /**
19
+ * In-memory event transport implementation using EventEmitter.
20
+ * Events are emitted to subscribers in the same process.
21
+ */
22
+ export class MemoryEventTransport implements EventTransport {
23
+ private emitter = new EventEmitter();
24
+
25
+ constructor() {
26
+ // Increase max listeners to avoid warnings in workflows with many steps
27
+ this.emitter.setMaxListeners(100);
28
+ }
29
+
30
+ /**
31
+ * Emit an event to all subscribers.
32
+ */
33
+ emit(event: WorkflowEvent): void {
34
+ // Emit to global subscribers
35
+ this.emitter.emit("event", event);
36
+
37
+ // Emit to run-specific subscribers
38
+ this.emitter.emit(`run:${event.runId}`, event);
39
+
40
+ // Emit to event-type subscribers
41
+ this.emitter.emit(`type:${event.eventType}`, event);
42
+ }
43
+
44
+ /**
45
+ * Subscribe to events for a specific run.
46
+ */
47
+ subscribe(runId: string, callback: EventCallback): Unsubscribe {
48
+ const channel = `run:${runId}`;
49
+ this.emitter.on(channel, callback);
50
+ return () => {
51
+ this.emitter.off(channel, callback);
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Subscribe to all events.
57
+ */
58
+ subscribeAll(callback: EventCallback): Unsubscribe {
59
+ this.emitter.on("event", callback);
60
+ return () => {
61
+ this.emitter.off("event", callback);
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Subscribe to events of a specific type.
67
+ */
68
+ subscribeToType(
69
+ eventType: WorkflowEventType,
70
+ callback: EventCallback,
71
+ ): Unsubscribe {
72
+ const channel = `type:${eventType}`;
73
+ this.emitter.on(channel, callback);
74
+ return () => {
75
+ this.emitter.off(channel, callback);
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Close the transport and remove all listeners.
81
+ */
82
+ close(): void {
83
+ this.emitter.removeAllListeners();
84
+ }
85
+
86
+ /**
87
+ * Get the number of listeners for a channel (for testing).
88
+ */
89
+ getListenerCount(channel?: string): number {
90
+ if (channel) {
91
+ return this.emitter.listenerCount(channel);
92
+ }
93
+ return this.emitter.listenerCount("event");
94
+ }
95
+ }