resumable-workflow 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Resumable Workflow
2
+
3
+ A lightweight, framework-agnostic Node.js library to ensure multi-step processes survive server crashes. It uses a declarative functional API and checkpoints progress to persistent storage after every step.
4
+
5
+ ## Features
6
+ - **Checkpointing:** Automatically saves state after each successful step.
7
+ - **Resumable:** Easily resume failed or interrupted runs by ID.
8
+ - **Result Pattern:** Returns descriptive objects instead of throwing errors for better control and performance.
9
+ - **Auto-Resume:** Optional background resumption of incomplete tasks on startup.
10
+ - **Pluggable Storage:** Default file-system storage included, with interfaces for Redis or SQL.
11
+
12
+ ## Architecture
13
+
14
+ The library is split into three main parts:
15
+ 1. **The Engine (`src/core`):** Manages the execution loop and ensures that steps are called in order.
16
+ 2. **Storage Providers (`src/storage`):** Handles the physical saving and loading of the workflow state.
17
+ 3. **Types (`src/types`):** Provides strict generic support so your input and output data are always type-safe.
18
+
19
+ ## Installation
20
+ ```bash
21
+ pnpm add resumable-workflow
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### 1. Define and Start a Workflow
27
+ ```typescript
28
+ import { createWorkflow } from 'resumable-workflow';
29
+
30
+ const onboarding = createWorkflow({
31
+ id: 'user-onboarding',
32
+ steps: [
33
+ {
34
+ name: 'create-user',
35
+ run: async ({ input }) => {
36
+ // Logic to create user
37
+ return { userId: '123' }; // Merged into state
38
+ }
39
+ },
40
+ {
41
+ name: 'send-welcome-email',
42
+ run: async ({ state }) => {
43
+ // Access state.userId from previous step
44
+ await mailer.send(state.userId);
45
+ return { emailSent: true };
46
+ }
47
+ }
48
+ ]
49
+ });
50
+
51
+ const response = await onboarding.start({ email: 'user@example.com' });
52
+
53
+ if (response.success) {
54
+ console.log('Workflow finished:', response.result);
55
+ } else {
56
+ console.error('Workflow failed at:', response.stepName, 'Error:', response.error);
57
+ // You can store response.runId to resume later
58
+ }
59
+ ```
60
+
61
+ ### 2. Resume a Failed Run
62
+ ```typescript
63
+ const response = await onboarding.resume(failedRunId);
64
+ ```
65
+
66
+ ### 3. Auto-Resume on Startup
67
+ ```typescript
68
+ const workflow = createWorkflow({
69
+ id: 'critical-process',
70
+ autoResume: true, // Will automatically scan and resume pending tasks
71
+ steps: [...]
72
+ });
73
+ ```
74
+
75
+ ## API
76
+
77
+ ### `createWorkflow(config)`
78
+ Returns an engine with `start`, `resume`, `listIncomplete`, and `resumeAllIncomplete`.
79
+
80
+ #### Config:
81
+ - `id`: Unique string identifying the workflow.
82
+ - `steps`: Array of step objects `{ name, run }`.
83
+ - `autoResume`: (Optional) Boolean.
84
+ - `storage`: (Optional) Custom storage provider.
85
+
86
+ ### The Result Object
87
+ All execution methods return a `WorkflowResult`:
88
+ - **Success:** `{ success: true, result: T, runId: string }`
89
+ - **Failure:** `{ success: false, error: string, runId: string, stepName: string }`
90
+
91
+ ## Development & Release
92
+
93
+ ### Workflow for Changes
94
+ 1. Make your changes.
95
+ 2. Run `pnpm changeset` and follow the prompts to describe your change.
96
+ 3. Commit the generated changeset file.
97
+
98
+ ### CI/CD
99
+ This project uses **GitHub Actions** for CI. Every pull request is automatically:
100
+ - Linted with Biome
101
+ - Built with tsup
102
+ - Tested with Vitest
103
+
104
+ ## Contributing
105
+
106
+ We welcome contributions! Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed instructions on how to set up the project, our coding standards, and the pull request process.
107
+
108
+ ## License
109
+ MIT
@@ -0,0 +1,67 @@
1
+ interface Step<TInput = unknown, TState = Record<string, unknown>, TOutput = unknown> {
2
+ readonly name: string;
3
+ readonly run: (context: {
4
+ readonly input: TInput;
5
+ readonly state: TState;
6
+ }) => Promise<TOutput>;
7
+ }
8
+ interface WorkflowConfig<TInput = unknown, TState = Record<string, unknown>> {
9
+ readonly id: string;
10
+ readonly steps: readonly Step<TInput, TState, unknown>[];
11
+ readonly autoResume?: boolean;
12
+ readonly storage?: StorageProvider;
13
+ }
14
+ type WorkflowStatus = 'pending' | 'completed' | 'failed';
15
+ interface WorkflowRunState<TInput = unknown, TState = Record<string, unknown>> {
16
+ readonly workflowId: string;
17
+ readonly runId: string;
18
+ readonly status: WorkflowStatus;
19
+ readonly currentStepIndex: number;
20
+ readonly input: TInput;
21
+ readonly state: TState;
22
+ readonly error?: string;
23
+ }
24
+ type WorkflowResult<T = Record<string, unknown>> = {
25
+ readonly success: true;
26
+ readonly result: T;
27
+ readonly runId: string;
28
+ } | {
29
+ readonly success: false;
30
+ readonly error: string;
31
+ readonly runId: string;
32
+ readonly stepName: string;
33
+ };
34
+ interface StorageProvider {
35
+ saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
36
+ getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
37
+ listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
38
+ }
39
+
40
+ declare class Workflow<TInput = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
41
+ private readonly storage;
42
+ private readonly workflowId;
43
+ private readonly config;
44
+ constructor(config: WorkflowConfig<TInput, TState>);
45
+ private executeStep;
46
+ private run;
47
+ start(input: TInput): Promise<WorkflowResult<TState>>;
48
+ resume(runId: string): Promise<WorkflowResult<TState>>;
49
+ listIncomplete(): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
50
+ resumeAllIncomplete(): Promise<WorkflowResult<TState>[]>;
51
+ }
52
+ /**
53
+ * Factory function for backward compatibility and ease of use
54
+ */
55
+ declare function createWorkflow<TInput = unknown, TState extends Record<string, unknown> = Record<string, unknown>>(config: WorkflowConfig<TInput, TState>): Workflow<TInput, TState>;
56
+
57
+ declare class FileStorage implements StorageProvider {
58
+ private readonly baseDir;
59
+ constructor(baseDir?: string);
60
+ private ensureDir;
61
+ private getFilePath;
62
+ saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
63
+ getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
64
+ listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
65
+ }
66
+
67
+ export { FileStorage, type Step, type StorageProvider, Workflow, type WorkflowConfig, type WorkflowResult, type WorkflowRunState, type WorkflowStatus, createWorkflow };
@@ -0,0 +1,67 @@
1
+ interface Step<TInput = unknown, TState = Record<string, unknown>, TOutput = unknown> {
2
+ readonly name: string;
3
+ readonly run: (context: {
4
+ readonly input: TInput;
5
+ readonly state: TState;
6
+ }) => Promise<TOutput>;
7
+ }
8
+ interface WorkflowConfig<TInput = unknown, TState = Record<string, unknown>> {
9
+ readonly id: string;
10
+ readonly steps: readonly Step<TInput, TState, unknown>[];
11
+ readonly autoResume?: boolean;
12
+ readonly storage?: StorageProvider;
13
+ }
14
+ type WorkflowStatus = 'pending' | 'completed' | 'failed';
15
+ interface WorkflowRunState<TInput = unknown, TState = Record<string, unknown>> {
16
+ readonly workflowId: string;
17
+ readonly runId: string;
18
+ readonly status: WorkflowStatus;
19
+ readonly currentStepIndex: number;
20
+ readonly input: TInput;
21
+ readonly state: TState;
22
+ readonly error?: string;
23
+ }
24
+ type WorkflowResult<T = Record<string, unknown>> = {
25
+ readonly success: true;
26
+ readonly result: T;
27
+ readonly runId: string;
28
+ } | {
29
+ readonly success: false;
30
+ readonly error: string;
31
+ readonly runId: string;
32
+ readonly stepName: string;
33
+ };
34
+ interface StorageProvider {
35
+ saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
36
+ getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
37
+ listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
38
+ }
39
+
40
+ declare class Workflow<TInput = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
41
+ private readonly storage;
42
+ private readonly workflowId;
43
+ private readonly config;
44
+ constructor(config: WorkflowConfig<TInput, TState>);
45
+ private executeStep;
46
+ private run;
47
+ start(input: TInput): Promise<WorkflowResult<TState>>;
48
+ resume(runId: string): Promise<WorkflowResult<TState>>;
49
+ listIncomplete(): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
50
+ resumeAllIncomplete(): Promise<WorkflowResult<TState>[]>;
51
+ }
52
+ /**
53
+ * Factory function for backward compatibility and ease of use
54
+ */
55
+ declare function createWorkflow<TInput = unknown, TState extends Record<string, unknown> = Record<string, unknown>>(config: WorkflowConfig<TInput, TState>): Workflow<TInput, TState>;
56
+
57
+ declare class FileStorage implements StorageProvider {
58
+ private readonly baseDir;
59
+ constructor(baseDir?: string);
60
+ private ensureDir;
61
+ private getFilePath;
62
+ saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
63
+ getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
64
+ listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
65
+ }
66
+
67
+ export { FileStorage, type Step, type StorageProvider, Workflow, type WorkflowConfig, type WorkflowResult, type WorkflowRunState, type WorkflowStatus, createWorkflow };
package/dist/index.js ADDED
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FileStorage: () => FileStorage,
34
+ Workflow: () => Workflow,
35
+ createWorkflow: () => createWorkflow
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/core/engine.ts
40
+ var import_node_crypto = require("crypto");
41
+
42
+ // src/storage/file-storage.ts
43
+ var import_promises = __toESM(require("fs/promises"));
44
+ var import_node_path = __toESM(require("path"));
45
+ var FileStorage = class {
46
+ constructor(baseDir = ".resumable-workflow") {
47
+ this.baseDir = import_node_path.default.resolve(baseDir);
48
+ }
49
+ async ensureDir() {
50
+ await import_promises.default.mkdir(this.baseDir, { recursive: true });
51
+ }
52
+ getFilePath(runId) {
53
+ return import_node_path.default.join(this.baseDir, `${runId}.json`);
54
+ }
55
+ async saveRun(state) {
56
+ await this.ensureDir();
57
+ await import_promises.default.writeFile(
58
+ this.getFilePath(state.runId),
59
+ JSON.stringify(state, null, 2)
60
+ );
61
+ }
62
+ async getRun(runId) {
63
+ try {
64
+ const data = await import_promises.default.readFile(this.getFilePath(runId), "utf-8");
65
+ return JSON.parse(data);
66
+ } catch (_e) {
67
+ return null;
68
+ }
69
+ }
70
+ async listIncompleteRuns(workflowId) {
71
+ try {
72
+ await this.ensureDir();
73
+ const files = await import_promises.default.readdir(this.baseDir);
74
+ const runs = [];
75
+ for (const file of files) {
76
+ if (file.endsWith(".json")) {
77
+ const data = await import_promises.default.readFile(
78
+ import_node_path.default.join(this.baseDir, file),
79
+ "utf-8"
80
+ );
81
+ const run = JSON.parse(data);
82
+ if (run.workflowId === workflowId && run.status === "pending") {
83
+ runs.push(run);
84
+ }
85
+ }
86
+ }
87
+ return runs;
88
+ } catch (_e) {
89
+ return [];
90
+ }
91
+ }
92
+ };
93
+
94
+ // src/core/engine.ts
95
+ var Workflow = class {
96
+ constructor(config) {
97
+ this.config = config;
98
+ this.storage = config.storage || new FileStorage();
99
+ this.workflowId = config.id;
100
+ if (config.autoResume) {
101
+ this.resumeAllIncomplete().catch((err) => {
102
+ console.error(
103
+ `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,
104
+ err
105
+ );
106
+ });
107
+ }
108
+ }
109
+ async executeStep(currentState, stepIndex) {
110
+ const step = this.config.steps[stepIndex];
111
+ if (!step) {
112
+ return;
113
+ }
114
+ try {
115
+ const result = await step.run({
116
+ input: currentState.input,
117
+ state: currentState.state
118
+ });
119
+ if (result !== null && typeof result === "object" && !Array.isArray(result)) {
120
+ currentState.state = {
121
+ ...currentState.state,
122
+ ...result
123
+ };
124
+ } else if (result !== void 0) {
125
+ currentState.state[step.name || `step_${stepIndex}`] = result;
126
+ }
127
+ currentState.currentStepIndex = stepIndex + 1;
128
+ if (currentState.currentStepIndex === this.config.steps.length) {
129
+ currentState.status = "completed";
130
+ }
131
+ await this.storage.saveRun(currentState);
132
+ } catch (error) {
133
+ currentState.status = "failed";
134
+ currentState.error = error instanceof Error ? error.message : String(error);
135
+ await this.storage.saveRun(currentState);
136
+ throw error;
137
+ }
138
+ }
139
+ async run(input, existingRun) {
140
+ const runId = existingRun?.runId || (0, import_node_crypto.randomUUID)();
141
+ const currentState = existingRun || {
142
+ workflowId: this.workflowId,
143
+ runId,
144
+ status: "pending",
145
+ currentStepIndex: 0,
146
+ input,
147
+ state: {}
148
+ };
149
+ if (!existingRun) {
150
+ await this.storage.saveRun(currentState);
151
+ }
152
+ try {
153
+ for (let i = currentState.currentStepIndex; i < this.config.steps.length; i++) {
154
+ await this.executeStep(currentState, i);
155
+ }
156
+ } catch (_error) {
157
+ return {
158
+ success: false,
159
+ error: currentState.error || "Unknown error",
160
+ runId: currentState.runId,
161
+ stepName: this.config.steps[currentState.currentStepIndex]?.name || `step_${currentState.currentStepIndex}`
162
+ };
163
+ }
164
+ return {
165
+ success: true,
166
+ result: currentState.state,
167
+ runId: currentState.runId
168
+ };
169
+ }
170
+ start(input) {
171
+ return this.run(input);
172
+ }
173
+ async resume(runId) {
174
+ const runState = await this.storage.getRun(runId);
175
+ if (!runState) {
176
+ return {
177
+ success: false,
178
+ error: `Run ${runId} not found`,
179
+ runId,
180
+ stepName: "unknown"
181
+ };
182
+ }
183
+ if (runState.status === "completed") {
184
+ return {
185
+ success: true,
186
+ result: runState.state,
187
+ runId: runState.runId
188
+ };
189
+ }
190
+ return this.run(
191
+ runState.input,
192
+ runState
193
+ );
194
+ }
195
+ listIncomplete() {
196
+ return this.storage.listIncompleteRuns(this.workflowId);
197
+ }
198
+ async resumeAllIncomplete() {
199
+ const incomplete = await this.storage.listIncompleteRuns(this.workflowId);
200
+ return Promise.all(
201
+ incomplete.map(
202
+ (runState) => this.run(
203
+ runState.input,
204
+ runState
205
+ )
206
+ )
207
+ );
208
+ }
209
+ };
210
+ function createWorkflow(config) {
211
+ return new Workflow(config);
212
+ }
213
+ // Annotate the CommonJS export names for ESM import in node:
214
+ 0 && (module.exports = {
215
+ FileStorage,
216
+ Workflow,
217
+ createWorkflow
218
+ });
219
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core/engine.ts","../src/storage/file-storage.ts"],"sourcesContent":["export { createWorkflow, Workflow } from './core/engine';\nexport { FileStorage } from './storage/file-storage';\nexport type {\n Step,\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from './types';\n","import { randomUUID } from 'node:crypto';\nimport { FileStorage } from '../storage/file-storage';\nimport type {\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from '../types';\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: T[P];\n};\n\nexport class Workflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n> {\n private readonly storage: StorageProvider;\n private readonly workflowId: string;\n private readonly config: WorkflowConfig<TInput, TState>;\n\n constructor(config: WorkflowConfig<TInput, TState>) {\n this.config = config;\n this.storage = config.storage || new FileStorage();\n this.workflowId = config.id;\n\n if (config.autoResume) {\n this.resumeAllIncomplete().catch((err) => {\n console.error(\n `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,\n err\n );\n });\n }\n }\n\n private async executeStep(\n currentState: Mutable<WorkflowRunState<TInput, TState>>,\n stepIndex: number\n ): Promise<void> {\n const step = this.config.steps[stepIndex];\n if (!step) {\n return;\n }\n\n try {\n const result = await step.run({\n input: currentState.input,\n state: currentState.state,\n });\n\n if (\n result !== null &&\n typeof result === 'object' &&\n !Array.isArray(result)\n ) {\n currentState.state = {\n ...currentState.state,\n ...(result as Partial<TState>),\n };\n } else if (result !== undefined) {\n (currentState.state as Record<string, unknown>)[\n step.name || `step_${stepIndex}`\n ] = result;\n }\n\n currentState.currentStepIndex = stepIndex + 1;\n\n if (currentState.currentStepIndex === this.config.steps.length) {\n currentState.status = 'completed';\n }\n\n await this.storage.saveRun(currentState);\n } catch (error) {\n currentState.status = 'failed';\n currentState.error =\n error instanceof Error ? error.message : String(error);\n await this.storage.saveRun(currentState);\n throw error;\n }\n }\n\n private async run(\n input: TInput,\n existingRun?: WorkflowRunState<TInput, TState>\n ): Promise<WorkflowResult<TState>> {\n const runId = existingRun?.runId || randomUUID();\n const currentState: Mutable<WorkflowRunState<TInput, TState>> =\n (existingRun as Mutable<WorkflowRunState<TInput, TState>>) || {\n workflowId: this.workflowId,\n runId,\n status: 'pending' as WorkflowStatus,\n currentStepIndex: 0,\n input,\n state: {} as TState,\n };\n\n if (!existingRun) {\n await this.storage.saveRun(currentState);\n }\n\n try {\n for (\n let i = currentState.currentStepIndex;\n i < this.config.steps.length;\n i++\n ) {\n await this.executeStep(currentState, i);\n }\n } catch (_error) {\n return {\n success: false,\n error: currentState.error || 'Unknown error',\n runId: currentState.runId,\n stepName:\n this.config.steps[currentState.currentStepIndex]?.name ||\n `step_${currentState.currentStepIndex}`,\n };\n }\n\n return {\n success: true,\n result: currentState.state,\n runId: currentState.runId,\n };\n }\n\n start(input: TInput): Promise<WorkflowResult<TState>> {\n return this.run(input);\n }\n\n async resume(runId: string): Promise<WorkflowResult<TState>> {\n const runState = await this.storage.getRun(runId);\n if (!runState) {\n return {\n success: false,\n error: `Run ${runId} not found`,\n runId,\n stepName: 'unknown',\n };\n }\n if (runState.status === 'completed') {\n return {\n success: true,\n result: runState.state as TState,\n runId: runState.runId,\n };\n }\n return this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n );\n }\n\n listIncomplete(): Promise<\n WorkflowRunState<unknown, Record<string, unknown>>[]\n > {\n return this.storage.listIncompleteRuns(this.workflowId);\n }\n\n async resumeAllIncomplete(): Promise<WorkflowResult<TState>[]> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n return Promise.all(\n incomplete.map((runState) =>\n this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n )\n )\n );\n }\n}\n\n/**\n * Factory function for backward compatibility and ease of use\n */\nexport function createWorkflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n>(config: WorkflowConfig<TInput, TState>) {\n return new Workflow(config);\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { StorageProvider, WorkflowRunState } from '../types';\n\nexport class FileStorage implements StorageProvider {\n private readonly baseDir: string;\n\n constructor(baseDir = '.resumable-workflow') {\n this.baseDir = path.resolve(baseDir);\n }\n\n private async ensureDir(): Promise<void> {\n await fs.mkdir(this.baseDir, { recursive: true });\n }\n\n private getFilePath(runId: string): string {\n return path.join(this.baseDir, `${runId}.json`);\n }\n\n async saveRun(\n state: WorkflowRunState<unknown, Record<string, unknown>>\n ): Promise<void> {\n await this.ensureDir();\n await fs.writeFile(\n this.getFilePath(state.runId),\n JSON.stringify(state, null, 2)\n );\n }\n\n async getRun(\n runId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null> {\n try {\n const data = await fs.readFile(this.getFilePath(runId), 'utf-8');\n return JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n } catch (_e) {\n // Return null if file doesn't exist or is invalid\n return null;\n }\n }\n\n async listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n try {\n await this.ensureDir();\n const files = await fs.readdir(this.baseDir);\n const runs: WorkflowRunState<unknown, Record<string, unknown>>[] = [];\n\n for (const file of files) {\n if (file.endsWith('.json')) {\n const data = await fs.readFile(\n path.join(this.baseDir, file),\n 'utf-8'\n );\n const run = JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n if (run.workflowId === workflowId && run.status === 'pending') {\n runs.push(run);\n }\n }\n }\n return runs;\n } catch (_e) {\n // Return empty array if directory doesn't exist or fails to read\n return [];\n }\n }\n}\n"],"mappings":"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","names":["path","fs"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,180 @@
1
+ // src/core/engine.ts
2
+ import { randomUUID } from "crypto";
3
+
4
+ // src/storage/file-storage.ts
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+ var FileStorage = class {
8
+ constructor(baseDir = ".resumable-workflow") {
9
+ this.baseDir = path.resolve(baseDir);
10
+ }
11
+ async ensureDir() {
12
+ await fs.mkdir(this.baseDir, { recursive: true });
13
+ }
14
+ getFilePath(runId) {
15
+ return path.join(this.baseDir, `${runId}.json`);
16
+ }
17
+ async saveRun(state) {
18
+ await this.ensureDir();
19
+ await fs.writeFile(
20
+ this.getFilePath(state.runId),
21
+ JSON.stringify(state, null, 2)
22
+ );
23
+ }
24
+ async getRun(runId) {
25
+ try {
26
+ const data = await fs.readFile(this.getFilePath(runId), "utf-8");
27
+ return JSON.parse(data);
28
+ } catch (_e) {
29
+ return null;
30
+ }
31
+ }
32
+ async listIncompleteRuns(workflowId) {
33
+ try {
34
+ await this.ensureDir();
35
+ const files = await fs.readdir(this.baseDir);
36
+ const runs = [];
37
+ for (const file of files) {
38
+ if (file.endsWith(".json")) {
39
+ const data = await fs.readFile(
40
+ path.join(this.baseDir, file),
41
+ "utf-8"
42
+ );
43
+ const run = JSON.parse(data);
44
+ if (run.workflowId === workflowId && run.status === "pending") {
45
+ runs.push(run);
46
+ }
47
+ }
48
+ }
49
+ return runs;
50
+ } catch (_e) {
51
+ return [];
52
+ }
53
+ }
54
+ };
55
+
56
+ // src/core/engine.ts
57
+ var Workflow = class {
58
+ constructor(config) {
59
+ this.config = config;
60
+ this.storage = config.storage || new FileStorage();
61
+ this.workflowId = config.id;
62
+ if (config.autoResume) {
63
+ this.resumeAllIncomplete().catch((err) => {
64
+ console.error(
65
+ `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,
66
+ err
67
+ );
68
+ });
69
+ }
70
+ }
71
+ async executeStep(currentState, stepIndex) {
72
+ const step = this.config.steps[stepIndex];
73
+ if (!step) {
74
+ return;
75
+ }
76
+ try {
77
+ const result = await step.run({
78
+ input: currentState.input,
79
+ state: currentState.state
80
+ });
81
+ if (result !== null && typeof result === "object" && !Array.isArray(result)) {
82
+ currentState.state = {
83
+ ...currentState.state,
84
+ ...result
85
+ };
86
+ } else if (result !== void 0) {
87
+ currentState.state[step.name || `step_${stepIndex}`] = result;
88
+ }
89
+ currentState.currentStepIndex = stepIndex + 1;
90
+ if (currentState.currentStepIndex === this.config.steps.length) {
91
+ currentState.status = "completed";
92
+ }
93
+ await this.storage.saveRun(currentState);
94
+ } catch (error) {
95
+ currentState.status = "failed";
96
+ currentState.error = error instanceof Error ? error.message : String(error);
97
+ await this.storage.saveRun(currentState);
98
+ throw error;
99
+ }
100
+ }
101
+ async run(input, existingRun) {
102
+ const runId = existingRun?.runId || randomUUID();
103
+ const currentState = existingRun || {
104
+ workflowId: this.workflowId,
105
+ runId,
106
+ status: "pending",
107
+ currentStepIndex: 0,
108
+ input,
109
+ state: {}
110
+ };
111
+ if (!existingRun) {
112
+ await this.storage.saveRun(currentState);
113
+ }
114
+ try {
115
+ for (let i = currentState.currentStepIndex; i < this.config.steps.length; i++) {
116
+ await this.executeStep(currentState, i);
117
+ }
118
+ } catch (_error) {
119
+ return {
120
+ success: false,
121
+ error: currentState.error || "Unknown error",
122
+ runId: currentState.runId,
123
+ stepName: this.config.steps[currentState.currentStepIndex]?.name || `step_${currentState.currentStepIndex}`
124
+ };
125
+ }
126
+ return {
127
+ success: true,
128
+ result: currentState.state,
129
+ runId: currentState.runId
130
+ };
131
+ }
132
+ start(input) {
133
+ return this.run(input);
134
+ }
135
+ async resume(runId) {
136
+ const runState = await this.storage.getRun(runId);
137
+ if (!runState) {
138
+ return {
139
+ success: false,
140
+ error: `Run ${runId} not found`,
141
+ runId,
142
+ stepName: "unknown"
143
+ };
144
+ }
145
+ if (runState.status === "completed") {
146
+ return {
147
+ success: true,
148
+ result: runState.state,
149
+ runId: runState.runId
150
+ };
151
+ }
152
+ return this.run(
153
+ runState.input,
154
+ runState
155
+ );
156
+ }
157
+ listIncomplete() {
158
+ return this.storage.listIncompleteRuns(this.workflowId);
159
+ }
160
+ async resumeAllIncomplete() {
161
+ const incomplete = await this.storage.listIncompleteRuns(this.workflowId);
162
+ return Promise.all(
163
+ incomplete.map(
164
+ (runState) => this.run(
165
+ runState.input,
166
+ runState
167
+ )
168
+ )
169
+ );
170
+ }
171
+ };
172
+ function createWorkflow(config) {
173
+ return new Workflow(config);
174
+ }
175
+ export {
176
+ FileStorage,
177
+ Workflow,
178
+ createWorkflow
179
+ };
180
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/engine.ts","../src/storage/file-storage.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { FileStorage } from '../storage/file-storage';\nimport type {\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from '../types';\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: T[P];\n};\n\nexport class Workflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n> {\n private readonly storage: StorageProvider;\n private readonly workflowId: string;\n private readonly config: WorkflowConfig<TInput, TState>;\n\n constructor(config: WorkflowConfig<TInput, TState>) {\n this.config = config;\n this.storage = config.storage || new FileStorage();\n this.workflowId = config.id;\n\n if (config.autoResume) {\n this.resumeAllIncomplete().catch((err) => {\n console.error(\n `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,\n err\n );\n });\n }\n }\n\n private async executeStep(\n currentState: Mutable<WorkflowRunState<TInput, TState>>,\n stepIndex: number\n ): Promise<void> {\n const step = this.config.steps[stepIndex];\n if (!step) {\n return;\n }\n\n try {\n const result = await step.run({\n input: currentState.input,\n state: currentState.state,\n });\n\n if (\n result !== null &&\n typeof result === 'object' &&\n !Array.isArray(result)\n ) {\n currentState.state = {\n ...currentState.state,\n ...(result as Partial<TState>),\n };\n } else if (result !== undefined) {\n (currentState.state as Record<string, unknown>)[\n step.name || `step_${stepIndex}`\n ] = result;\n }\n\n currentState.currentStepIndex = stepIndex + 1;\n\n if (currentState.currentStepIndex === this.config.steps.length) {\n currentState.status = 'completed';\n }\n\n await this.storage.saveRun(currentState);\n } catch (error) {\n currentState.status = 'failed';\n currentState.error =\n error instanceof Error ? error.message : String(error);\n await this.storage.saveRun(currentState);\n throw error;\n }\n }\n\n private async run(\n input: TInput,\n existingRun?: WorkflowRunState<TInput, TState>\n ): Promise<WorkflowResult<TState>> {\n const runId = existingRun?.runId || randomUUID();\n const currentState: Mutable<WorkflowRunState<TInput, TState>> =\n (existingRun as Mutable<WorkflowRunState<TInput, TState>>) || {\n workflowId: this.workflowId,\n runId,\n status: 'pending' as WorkflowStatus,\n currentStepIndex: 0,\n input,\n state: {} as TState,\n };\n\n if (!existingRun) {\n await this.storage.saveRun(currentState);\n }\n\n try {\n for (\n let i = currentState.currentStepIndex;\n i < this.config.steps.length;\n i++\n ) {\n await this.executeStep(currentState, i);\n }\n } catch (_error) {\n return {\n success: false,\n error: currentState.error || 'Unknown error',\n runId: currentState.runId,\n stepName:\n this.config.steps[currentState.currentStepIndex]?.name ||\n `step_${currentState.currentStepIndex}`,\n };\n }\n\n return {\n success: true,\n result: currentState.state,\n runId: currentState.runId,\n };\n }\n\n start(input: TInput): Promise<WorkflowResult<TState>> {\n return this.run(input);\n }\n\n async resume(runId: string): Promise<WorkflowResult<TState>> {\n const runState = await this.storage.getRun(runId);\n if (!runState) {\n return {\n success: false,\n error: `Run ${runId} not found`,\n runId,\n stepName: 'unknown',\n };\n }\n if (runState.status === 'completed') {\n return {\n success: true,\n result: runState.state as TState,\n runId: runState.runId,\n };\n }\n return this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n );\n }\n\n listIncomplete(): Promise<\n WorkflowRunState<unknown, Record<string, unknown>>[]\n > {\n return this.storage.listIncompleteRuns(this.workflowId);\n }\n\n async resumeAllIncomplete(): Promise<WorkflowResult<TState>[]> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n return Promise.all(\n incomplete.map((runState) =>\n this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n )\n )\n );\n }\n}\n\n/**\n * Factory function for backward compatibility and ease of use\n */\nexport function createWorkflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n>(config: WorkflowConfig<TInput, TState>) {\n return new Workflow(config);\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { StorageProvider, WorkflowRunState } from '../types';\n\nexport class FileStorage implements StorageProvider {\n private readonly baseDir: string;\n\n constructor(baseDir = '.resumable-workflow') {\n this.baseDir = path.resolve(baseDir);\n }\n\n private async ensureDir(): Promise<void> {\n await fs.mkdir(this.baseDir, { recursive: true });\n }\n\n private getFilePath(runId: string): string {\n return path.join(this.baseDir, `${runId}.json`);\n }\n\n async saveRun(\n state: WorkflowRunState<unknown, Record<string, unknown>>\n ): Promise<void> {\n await this.ensureDir();\n await fs.writeFile(\n this.getFilePath(state.runId),\n JSON.stringify(state, null, 2)\n );\n }\n\n async getRun(\n runId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null> {\n try {\n const data = await fs.readFile(this.getFilePath(runId), 'utf-8');\n return JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n } catch (_e) {\n // Return null if file doesn't exist or is invalid\n return null;\n }\n }\n\n async listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n try {\n await this.ensureDir();\n const files = await fs.readdir(this.baseDir);\n const runs: WorkflowRunState<unknown, Record<string, unknown>>[] = [];\n\n for (const file of files) {\n if (file.endsWith('.json')) {\n const data = await fs.readFile(\n path.join(this.baseDir, file),\n 'utf-8'\n );\n const run = JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n if (run.workflowId === workflowId && run.status === 'pending') {\n runs.push(run);\n }\n }\n }\n return runs;\n } catch (_e) {\n // Return empty array if directory doesn't exist or fails to read\n return [];\n }\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACA3B,OAAO,QAAQ;AACf,OAAO,UAAU;AAGV,IAAM,cAAN,MAA6C;AAAA,EAGlD,YAAY,UAAU,uBAAuB;AAC3C,SAAK,UAAU,KAAK,QAAQ,OAAO;AAAA,EACrC;AAAA,EAEA,MAAc,YAA2B;AACvC,UAAM,GAAG,MAAM,KAAK,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,EAClD;AAAA,EAEQ,YAAY,OAAuB;AACzC,WAAO,KAAK,KAAK,KAAK,SAAS,GAAG,KAAK,OAAO;AAAA,EAChD;AAAA,EAEA,MAAM,QACJ,OACe;AACf,UAAM,KAAK,UAAU;AACrB,UAAM,GAAG;AAAA,MACP,KAAK,YAAY,MAAM,KAAK;AAAA,MAC5B,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,MAAM,OACJ,OACoE;AACpE,QAAI;AACF,YAAM,OAAO,MAAM,GAAG,SAAS,KAAK,YAAY,KAAK,GAAG,OAAO;AAC/D,aAAO,KAAK,MAAM,IAAI;AAAA,IAIxB,SAAS,IAAI;AAEX,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,mBACJ,YAC+D;AAC/D,QAAI;AACF,YAAM,KAAK,UAAU;AACrB,YAAM,QAAQ,MAAM,GAAG,QAAQ,KAAK,OAAO;AAC3C,YAAM,OAA6D,CAAC;AAEpE,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,SAAS,OAAO,GAAG;AAC1B,gBAAM,OAAO,MAAM,GAAG;AAAA,YACpB,KAAK,KAAK,KAAK,SAAS,IAAI;AAAA,YAC5B;AAAA,UACF;AACA,gBAAM,MAAM,KAAK,MAAM,IAAI;AAI3B,cAAI,IAAI,eAAe,cAAc,IAAI,WAAW,WAAW;AAC7D,iBAAK,KAAK,GAAG;AAAA,UACf;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT,SAAS,IAAI;AAEX,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AD3DO,IAAM,WAAN,MAGL;AAAA,EAKA,YAAY,QAAwC;AAClD,SAAK,SAAS;AACd,SAAK,UAAU,OAAO,WAAW,IAAI,YAAY;AACjD,SAAK,aAAa,OAAO;AAEzB,QAAI,OAAO,YAAY;AACrB,WAAK,oBAAoB,EAAE,MAAM,CAAC,QAAQ;AACxC,gBAAQ;AAAA,UACN,uBAAuB,KAAK,UAAU;AAAA,UACtC;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,YACZ,cACA,WACe;AACf,UAAM,OAAO,KAAK,OAAO,MAAM,SAAS;AACxC,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,IAAI;AAAA,QAC5B,OAAO,aAAa;AAAA,QACpB,OAAO,aAAa;AAAA,MACtB,CAAC;AAED,UACE,WAAW,QACX,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,GACrB;AACA,qBAAa,QAAQ;AAAA,UACnB,GAAG,aAAa;AAAA,UAChB,GAAI;AAAA,QACN;AAAA,MACF,WAAW,WAAW,QAAW;AAC/B,QAAC,aAAa,MACZ,KAAK,QAAQ,QAAQ,SAAS,EAChC,IAAI;AAAA,MACN;AAEA,mBAAa,mBAAmB,YAAY;AAE5C,UAAI,aAAa,qBAAqB,KAAK,OAAO,MAAM,QAAQ;AAC9D,qBAAa,SAAS;AAAA,MACxB;AAEA,YAAM,KAAK,QAAQ,QAAQ,YAAY;AAAA,IACzC,SAAS,OAAO;AACd,mBAAa,SAAS;AACtB,mBAAa,QACX,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACvD,YAAM,KAAK,QAAQ,QAAQ,YAAY;AACvC,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAc,IACZ,OACA,aACiC;AACjC,UAAM,QAAQ,aAAa,SAAS,WAAW;AAC/C,UAAM,eACH,eAA6D;AAAA,MAC5D,YAAY,KAAK;AAAA,MACjB;AAAA,MACA,QAAQ;AAAA,MACR,kBAAkB;AAAA,MAClB;AAAA,MACA,OAAO,CAAC;AAAA,IACV;AAEF,QAAI,CAAC,aAAa;AAChB,YAAM,KAAK,QAAQ,QAAQ,YAAY;AAAA,IACzC;AAEA,QAAI;AACF,eACM,IAAI,aAAa,kBACrB,IAAI,KAAK,OAAO,MAAM,QACtB,KACA;AACA,cAAM,KAAK,YAAY,cAAc,CAAC;AAAA,MACxC;AAAA,IACF,SAAS,QAAQ;AACf,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,aAAa,SAAS;AAAA,QAC7B,OAAO,aAAa;AAAA,QACpB,UACE,KAAK,OAAO,MAAM,aAAa,gBAAgB,GAAG,QAClD,QAAQ,aAAa,gBAAgB;AAAA,MACzC;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ,aAAa;AAAA,MACrB,OAAO,aAAa;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,OAAgD;AACpD,WAAO,KAAK,IAAI,KAAK;AAAA,EACvB;AAAA,EAEA,MAAM,OAAO,OAAgD;AAC3D,UAAM,WAAW,MAAM,KAAK,QAAQ,OAAO,KAAK;AAChD,QAAI,CAAC,UAAU;AACb,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,OAAO,KAAK;AAAA,QACnB;AAAA,QACA,UAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,SAAS,WAAW,aAAa;AACnC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ,SAAS;AAAA,QACjB,OAAO,SAAS;AAAA,MAClB;AAAA,IACF;AACA,WAAO,KAAK;AAAA,MACV,SAAS;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EAEA,iBAEE;AACA,WAAO,KAAK,QAAQ,mBAAmB,KAAK,UAAU;AAAA,EACxD;AAAA,EAEA,MAAM,sBAAyD;AAC7D,UAAM,aAAa,MAAM,KAAK,QAAQ,mBAAmB,KAAK,UAAU;AACxE,WAAO,QAAQ;AAAA,MACb,WAAW;AAAA,QAAI,CAAC,aACd,KAAK;AAAA,UACH,SAAS;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAKO,SAAS,eAGd,QAAwC;AACxC,SAAO,IAAI,SAAS,MAAM;AAC5B;","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "resumable-workflow",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, persistent workflow engine for Node.js. Ensure multi-step processes survive crashes and resume automatically.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "keywords": [
12
+ "workflow",
13
+ "resumable",
14
+ "persistent",
15
+ "checkpoint",
16
+ "saga",
17
+ "state-machine",
18
+ "reliability",
19
+ "nodejs",
20
+ "typescript"
21
+ ],
22
+ "author": "Mohamed Ragab",
23
+ "license": "MIT",
24
+ "devDependencies": {
25
+ "@biomejs/biome": "2.3.13",
26
+ "@changesets/cli": "^2.29.8",
27
+ "@types/node": "22.13.1",
28
+ "tsup": "8.5.1",
29
+ "typescript": "5.7.3",
30
+ "ultracite": "7.1.1",
31
+ "vitest": "3.0.5"
32
+ },
33
+ "scripts": {
34
+ "test": "vitest run",
35
+ "build": "tsup",
36
+ "lint": "biome lint .",
37
+ "format": "biome format . --write",
38
+ "check": "biome check --write .",
39
+ "version": "changeset version",
40
+ "release": "pnpm build && changeset publish"
41
+ }
42
+ }