resumable-workflow 1.1.0 → 1.1.3

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.
@@ -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" || run.status === "failed")) {
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
package/dist/index.js CHANGED
@@ -146,6 +146,9 @@ var Workflow = class {
146
146
  input,
147
147
  state: {}
148
148
  };
149
+ if (existingRun && existingRun.status !== "completed") {
150
+ currentState.status = "pending";
151
+ }
149
152
  if (!existingRun) {
150
153
  await this.storage.saveRun(currentState);
151
154
  }
package/dist/index.js.map CHANGED
@@ -1 +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 (\n run.workflowId === workflowId &&\n (run.status === 'pending' || run.status === 'failed')\n ) {\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"]}
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 resuming a failed/halted run, reset its status to pending\n if (existingRun && existingRun.status !== 'completed') {\n currentState.status = 'pending';\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 (\n run.workflowId === workflowId &&\n (run.status === 'pending' || run.status === 'failed')\n ) {\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 CHANGED
@@ -108,6 +108,9 @@ var Workflow = class {
108
108
  input,
109
109
  state: {}
110
110
  };
111
+ if (existingRun && existingRun.status !== "completed") {
112
+ currentState.status = "pending";
113
+ }
111
114
  if (!existingRun) {
112
115
  await this.storage.saveRun(currentState);
113
116
  }
@@ -1 +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 (\n run.workflowId === workflowId &&\n (run.status === 'pending' || run.status === 'failed')\n ) {\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":[]}
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 resuming a failed/halted run, reset its status to pending\n if (existingRun && existingRun.status !== 'completed') {\n currentState.status = 'pending';\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 (\n run.workflowId === workflowId &&\n (run.status === 'pending' || run.status === 'failed')\n ) {\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,cACE,IAAI,eAAe,eAClB,IAAI,WAAW,aAAa,IAAI,WAAW,WAC5C;AACA,iBAAK,KAAK,GAAG;AAAA,UACf;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT,SAAS,IAAI;AAEX,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AD9DO,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;AAGF,QAAI,eAAe,YAAY,WAAW,aAAa;AACrD,mBAAa,SAAS;AAAA,IACxB;AAEA,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 CHANGED
@@ -1,13 +1,28 @@
1
1
  {
2
2
  "name": "resumable-workflow",
3
- "version": "1.1.0",
3
+ "version": "1.1.3",
4
4
  "description": "A lightweight, persistent workflow engine for Node.js. Ensure multi-step processes survive crashes and resume automatically.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
8
15
  "files": [
9
16
  "dist"
10
17
  ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Muhamed-Ragab/resumable-workflow.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/Muhamed-Ragab/resumable-workflow/issues"
24
+ },
25
+ "homepage": "https://github.com/Muhamed-Ragab/resumable-workflow#readme",
11
26
  "keywords": [
12
27
  "workflow",
13
28
  "resumable",