resumable-workflow 1.3.0 → 1.4.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/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/index.d.mts +11 -7
- package/dist/index.d.ts +11 -7
- package/dist/index.js +30 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohamed Ragab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A lightweight, framework-agnostic Node.js library to ensure multi-step processes
|
|
|
7
7
|
- **Resumable:** Easily resume failed or interrupted runs by ID.
|
|
8
8
|
- **Result Pattern:** Returns descriptive objects instead of throwing errors for better control and performance.
|
|
9
9
|
- **Auto-Resume:** Optional background resumption of incomplete tasks on startup.
|
|
10
|
+
- **Auto-Cleanup:** Optional automatic deletion of run data upon successful completion.
|
|
10
11
|
- **Pluggable Storage:** Default file-system storage included, with interfaces for Redis or SQL.
|
|
11
12
|
|
|
12
13
|
## Architecture
|
|
@@ -29,6 +30,7 @@ import { createWorkflow } from 'resumable-workflow';
|
|
|
29
30
|
|
|
30
31
|
const onboarding = createWorkflow({
|
|
31
32
|
id: 'user-onboarding',
|
|
33
|
+
autoCleanup: true, // Optional: Automatically delete data after success
|
|
32
34
|
steps: [
|
|
33
35
|
{
|
|
34
36
|
name: 'create-user',
|
|
@@ -37,6 +39,20 @@ const onboarding = createWorkflow({
|
|
|
37
39
|
return { userId: '123' }; // Merged into state
|
|
38
40
|
}
|
|
39
41
|
},
|
|
42
|
+
// ...
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Manual Cleanup
|
|
50
|
+
You can also manually clear all completed runs for a workflow:
|
|
51
|
+
```typescript
|
|
52
|
+
await onboarding.clearCompleted();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Resume a Failed Run
|
|
40
56
|
{
|
|
41
57
|
name: 'send-welcome-email',
|
|
42
58
|
run: async ({ state }) => {
|
package/dist/index.d.mts
CHANGED
|
@@ -9,6 +9,10 @@ interface WorkflowConfig<TInput = unknown, TState = Record<string, unknown>> {
|
|
|
9
9
|
readonly id: string;
|
|
10
10
|
readonly steps: readonly Step<TInput, TState, unknown>[];
|
|
11
11
|
readonly autoResume?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* If true, the workflow run data will be deleted from storage immediately upon completion.
|
|
14
|
+
*/
|
|
15
|
+
readonly autoCleanup?: boolean;
|
|
12
16
|
readonly storage?: StorageProvider;
|
|
13
17
|
}
|
|
14
18
|
type WorkflowStatus = 'pending' | 'completed' | 'failed';
|
|
@@ -35,6 +39,8 @@ interface StorageProvider {
|
|
|
35
39
|
saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
|
|
36
40
|
getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
|
|
37
41
|
listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
42
|
+
listCompletedRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
43
|
+
deleteRun(runId: string): Promise<void>;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
declare class Workflow<TInput = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -46,17 +52,12 @@ declare class Workflow<TInput = unknown, TState extends Record<string, unknown>
|
|
|
46
52
|
private run;
|
|
47
53
|
start(input: TInput): Promise<WorkflowResult<TState>>;
|
|
48
54
|
resume(runId: string): Promise<WorkflowResult<TState>>;
|
|
49
|
-
/**
|
|
50
|
-
* Retrieves the current state of a specific run
|
|
51
|
-
*/
|
|
52
|
-
getRun(runId: string): Promise<WorkflowRunState<TInput, TState> | null>;
|
|
53
55
|
listIncomplete(): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
54
56
|
resumeAllIncomplete(): Promise<WorkflowResult<TState>[]>;
|
|
55
57
|
/**
|
|
56
|
-
*
|
|
57
|
-
* Useful for manual cleanup of stuck processes.
|
|
58
|
+
* Manually clears all completed workflow runs for this workflow ID from storage.
|
|
58
59
|
*/
|
|
59
|
-
|
|
60
|
+
clearCompleted(): Promise<void>;
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* Factory function for backward compatibility and ease of use
|
|
@@ -71,6 +72,9 @@ declare class FileStorage implements StorageProvider {
|
|
|
71
72
|
saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
|
|
72
73
|
getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
|
|
73
74
|
listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
75
|
+
listCompletedRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
76
|
+
private listRuns;
|
|
77
|
+
deleteRun(runId: string): Promise<void>;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
export { FileStorage, type Step, type StorageProvider, Workflow, type WorkflowConfig, type WorkflowResult, type WorkflowRunState, type WorkflowStatus, createWorkflow };
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ interface WorkflowConfig<TInput = unknown, TState = Record<string, unknown>> {
|
|
|
9
9
|
readonly id: string;
|
|
10
10
|
readonly steps: readonly Step<TInput, TState, unknown>[];
|
|
11
11
|
readonly autoResume?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* If true, the workflow run data will be deleted from storage immediately upon completion.
|
|
14
|
+
*/
|
|
15
|
+
readonly autoCleanup?: boolean;
|
|
12
16
|
readonly storage?: StorageProvider;
|
|
13
17
|
}
|
|
14
18
|
type WorkflowStatus = 'pending' | 'completed' | 'failed';
|
|
@@ -35,6 +39,8 @@ interface StorageProvider {
|
|
|
35
39
|
saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
|
|
36
40
|
getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
|
|
37
41
|
listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
42
|
+
listCompletedRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
43
|
+
deleteRun(runId: string): Promise<void>;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
declare class Workflow<TInput = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -46,17 +52,12 @@ declare class Workflow<TInput = unknown, TState extends Record<string, unknown>
|
|
|
46
52
|
private run;
|
|
47
53
|
start(input: TInput): Promise<WorkflowResult<TState>>;
|
|
48
54
|
resume(runId: string): Promise<WorkflowResult<TState>>;
|
|
49
|
-
/**
|
|
50
|
-
* Retrieves the current state of a specific run
|
|
51
|
-
*/
|
|
52
|
-
getRun(runId: string): Promise<WorkflowRunState<TInput, TState> | null>;
|
|
53
55
|
listIncomplete(): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
54
56
|
resumeAllIncomplete(): Promise<WorkflowResult<TState>[]>;
|
|
55
57
|
/**
|
|
56
|
-
*
|
|
57
|
-
* Useful for manual cleanup of stuck processes.
|
|
58
|
+
* Manually clears all completed workflow runs for this workflow ID from storage.
|
|
58
59
|
*/
|
|
59
|
-
|
|
60
|
+
clearCompleted(): Promise<void>;
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* Factory function for backward compatibility and ease of use
|
|
@@ -71,6 +72,9 @@ declare class FileStorage implements StorageProvider {
|
|
|
71
72
|
saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
|
|
72
73
|
getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
|
|
73
74
|
listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
75
|
+
listCompletedRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
|
76
|
+
private listRuns;
|
|
77
|
+
deleteRun(runId: string): Promise<void>;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
export { FileStorage, type Step, type StorageProvider, Workflow, type WorkflowConfig, type WorkflowResult, type WorkflowRunState, type WorkflowStatus, createWorkflow };
|
package/dist/index.js
CHANGED
|
@@ -67,20 +67,29 @@ var FileStorage = class {
|
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
listIncompleteRuns(workflowId) {
|
|
71
|
+
return this.listRuns(workflowId, "pending");
|
|
72
|
+
}
|
|
73
|
+
listCompletedRuns(workflowId) {
|
|
74
|
+
return this.listRuns(workflowId, "completed");
|
|
75
|
+
}
|
|
76
|
+
async listRuns(workflowId, status) {
|
|
71
77
|
try {
|
|
72
78
|
await this.ensureDir();
|
|
73
79
|
const files = await import_promises.default.readdir(this.baseDir);
|
|
74
80
|
const runs = [];
|
|
75
81
|
for (const file of files) {
|
|
76
82
|
if (file.endsWith(".json")) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
try {
|
|
84
|
+
const data = await import_promises.default.readFile(
|
|
85
|
+
import_node_path.default.join(this.baseDir, file),
|
|
86
|
+
"utf-8"
|
|
87
|
+
);
|
|
88
|
+
const run = JSON.parse(data);
|
|
89
|
+
if (run.workflowId === workflowId && run.status === status) {
|
|
90
|
+
runs.push(run);
|
|
91
|
+
}
|
|
92
|
+
} catch (_e) {
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
}
|
|
@@ -89,6 +98,12 @@ var FileStorage = class {
|
|
|
89
98
|
return [];
|
|
90
99
|
}
|
|
91
100
|
}
|
|
101
|
+
async deleteRun(runId) {
|
|
102
|
+
try {
|
|
103
|
+
await import_promises.default.unlink(this.getFilePath(runId));
|
|
104
|
+
} catch (_e) {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
92
107
|
};
|
|
93
108
|
|
|
94
109
|
// src/core/engine.ts
|
|
@@ -146,9 +161,6 @@ var Workflow = class {
|
|
|
146
161
|
input,
|
|
147
162
|
state: {}
|
|
148
163
|
};
|
|
149
|
-
if (existingRun && existingRun.status !== "completed") {
|
|
150
|
-
currentState.status = "pending";
|
|
151
|
-
}
|
|
152
164
|
if (!existingRun) {
|
|
153
165
|
await this.storage.saveRun(currentState);
|
|
154
166
|
}
|
|
@@ -164,6 +176,9 @@ var Workflow = class {
|
|
|
164
176
|
stepName: this.config.steps[currentState.currentStepIndex]?.name || `step_${currentState.currentStepIndex}`
|
|
165
177
|
};
|
|
166
178
|
}
|
|
179
|
+
if (this.config.autoCleanup) {
|
|
180
|
+
await this.storage.deleteRun(currentState.runId);
|
|
181
|
+
}
|
|
167
182
|
return {
|
|
168
183
|
success: true,
|
|
169
184
|
result: currentState.state,
|
|
@@ -195,13 +210,6 @@ var Workflow = class {
|
|
|
195
210
|
runState
|
|
196
211
|
);
|
|
197
212
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Retrieves the current state of a specific run
|
|
200
|
-
*/
|
|
201
|
-
async getRun(runId) {
|
|
202
|
-
const runState = await this.storage.getRun(runId);
|
|
203
|
-
return runState;
|
|
204
|
-
}
|
|
205
213
|
listIncomplete() {
|
|
206
214
|
return this.storage.listIncompleteRuns(this.workflowId);
|
|
207
215
|
}
|
|
@@ -217,18 +225,11 @@ var Workflow = class {
|
|
|
217
225
|
);
|
|
218
226
|
}
|
|
219
227
|
/**
|
|
220
|
-
*
|
|
221
|
-
* Useful for manual cleanup of stuck processes.
|
|
228
|
+
* Manually clears all completed workflow runs for this workflow ID from storage.
|
|
222
229
|
*/
|
|
223
|
-
async
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
const path2 = await import("path");
|
|
227
|
-
for (const run of incomplete) {
|
|
228
|
-
const filePath = path2.join(".resumable-workflow", `${run.runId}.json`);
|
|
229
|
-
await fs2.unlink(filePath).catch(() => {
|
|
230
|
-
});
|
|
231
|
-
}
|
|
230
|
+
async clearCompleted() {
|
|
231
|
+
const completed = await this.storage.listCompletedRuns(this.workflowId);
|
|
232
|
+
await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));
|
|
232
233
|
}
|
|
233
234
|
};
|
|
234
235
|
function createWorkflow(config) {
|
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 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 /**\n * Retrieves the current state of a specific run\n */\n async getRun(runId: string): Promise<WorkflowRunState<TInput, TState> | null> {\n const runState = await this.storage.getRun(runId);\n return runState as WorkflowRunState<TInput, TState> | null;\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 * Clears all incomplete runs for this workflow from storage.\n * Useful for manual cleanup of stuck processes.\n */\n async clearIncomplete(): Promise<void> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n const fs = await import('node:fs/promises');\n const path = await import('node:path');\n \n // Note: This assumes FileStorage. For custom storage, \n // we should ideally add a deleteRun method to the StorageProvider interface.\n // For now, we'll implement a safe cleanup.\n for (const run of incomplete) {\n const filePath = path.join('.resumable-workflow', `${run.runId}.json`);\n await fs.unlink(filePath).catch(() => {});\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","fs","path"]}
|
|
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 // Auto-cleanup if enabled and successful\n if (this.config.autoCleanup) {\n await this.storage.deleteRun(currentState.runId);\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 * Manually clears all completed workflow runs for this workflow ID from storage.\n */\n async clearCompleted(): Promise<void> {\n const completed = await this.storage.listCompletedRuns(this.workflowId);\n await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));\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}","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 listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'pending');\n }\n\n listCompletedRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'completed');\n }\n\n private async listRuns(\n workflowId: string,\n status: 'pending' | 'completed'\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 try {\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 === status) {\n runs.push(run);\n }\n } catch (_e) {\n // Ignore corrupted files\n }\n }\n }\n return runs;\n } catch (_e) {\n return [];\n }\n }\n\n async deleteRun(runId: string): Promise<void> {\n try {\n await fs.unlink(this.getFilePath(runId));\n } catch (_e) {\n // Ignore if file doesn't exist\n }\n }\n}\n"],"mappings":"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","names":["path","fs"]}
|
package/dist/index.mjs
CHANGED
|
@@ -29,20 +29,29 @@ var FileStorage = class {
|
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
listIncompleteRuns(workflowId) {
|
|
33
|
+
return this.listRuns(workflowId, "pending");
|
|
34
|
+
}
|
|
35
|
+
listCompletedRuns(workflowId) {
|
|
36
|
+
return this.listRuns(workflowId, "completed");
|
|
37
|
+
}
|
|
38
|
+
async listRuns(workflowId, status) {
|
|
33
39
|
try {
|
|
34
40
|
await this.ensureDir();
|
|
35
41
|
const files = await fs.readdir(this.baseDir);
|
|
36
42
|
const runs = [];
|
|
37
43
|
for (const file of files) {
|
|
38
44
|
if (file.endsWith(".json")) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
try {
|
|
46
|
+
const data = await fs.readFile(
|
|
47
|
+
path.join(this.baseDir, file),
|
|
48
|
+
"utf-8"
|
|
49
|
+
);
|
|
50
|
+
const run = JSON.parse(data);
|
|
51
|
+
if (run.workflowId === workflowId && run.status === status) {
|
|
52
|
+
runs.push(run);
|
|
53
|
+
}
|
|
54
|
+
} catch (_e) {
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
}
|
|
@@ -51,6 +60,12 @@ var FileStorage = class {
|
|
|
51
60
|
return [];
|
|
52
61
|
}
|
|
53
62
|
}
|
|
63
|
+
async deleteRun(runId) {
|
|
64
|
+
try {
|
|
65
|
+
await fs.unlink(this.getFilePath(runId));
|
|
66
|
+
} catch (_e) {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
54
69
|
};
|
|
55
70
|
|
|
56
71
|
// src/core/engine.ts
|
|
@@ -108,9 +123,6 @@ var Workflow = class {
|
|
|
108
123
|
input,
|
|
109
124
|
state: {}
|
|
110
125
|
};
|
|
111
|
-
if (existingRun && existingRun.status !== "completed") {
|
|
112
|
-
currentState.status = "pending";
|
|
113
|
-
}
|
|
114
126
|
if (!existingRun) {
|
|
115
127
|
await this.storage.saveRun(currentState);
|
|
116
128
|
}
|
|
@@ -126,6 +138,9 @@ var Workflow = class {
|
|
|
126
138
|
stepName: this.config.steps[currentState.currentStepIndex]?.name || `step_${currentState.currentStepIndex}`
|
|
127
139
|
};
|
|
128
140
|
}
|
|
141
|
+
if (this.config.autoCleanup) {
|
|
142
|
+
await this.storage.deleteRun(currentState.runId);
|
|
143
|
+
}
|
|
129
144
|
return {
|
|
130
145
|
success: true,
|
|
131
146
|
result: currentState.state,
|
|
@@ -157,13 +172,6 @@ var Workflow = class {
|
|
|
157
172
|
runState
|
|
158
173
|
);
|
|
159
174
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Retrieves the current state of a specific run
|
|
162
|
-
*/
|
|
163
|
-
async getRun(runId) {
|
|
164
|
-
const runState = await this.storage.getRun(runId);
|
|
165
|
-
return runState;
|
|
166
|
-
}
|
|
167
175
|
listIncomplete() {
|
|
168
176
|
return this.storage.listIncompleteRuns(this.workflowId);
|
|
169
177
|
}
|
|
@@ -179,18 +187,11 @@ var Workflow = class {
|
|
|
179
187
|
);
|
|
180
188
|
}
|
|
181
189
|
/**
|
|
182
|
-
*
|
|
183
|
-
* Useful for manual cleanup of stuck processes.
|
|
190
|
+
* Manually clears all completed workflow runs for this workflow ID from storage.
|
|
184
191
|
*/
|
|
185
|
-
async
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const path2 = await import("path");
|
|
189
|
-
for (const run of incomplete) {
|
|
190
|
-
const filePath = path2.join(".resumable-workflow", `${run.runId}.json`);
|
|
191
|
-
await fs2.unlink(filePath).catch(() => {
|
|
192
|
-
});
|
|
193
|
-
}
|
|
192
|
+
async clearCompleted() {
|
|
193
|
+
const completed = await this.storage.listCompletedRuns(this.workflowId);
|
|
194
|
+
await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));
|
|
194
195
|
}
|
|
195
196
|
};
|
|
196
197
|
function createWorkflow(config) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 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 /**\n * Retrieves the current state of a specific run\n */\n async getRun(runId: string): Promise<WorkflowRunState<TInput, TState> | null> {\n const runState = await this.storage.getRun(runId);\n return runState as WorkflowRunState<TInput, TState> | null;\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 * Clears all incomplete runs for this workflow from storage.\n * Useful for manual cleanup of stuck processes.\n */\n async clearIncomplete(): Promise<void> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n const fs = await import('node:fs/promises');\n const path = await import('node:path');\n \n // Note: This assumes FileStorage. For custom storage, \n // we should ideally add a deleteRun method to the StorageProvider interface.\n // For now, we'll implement a safe cleanup.\n for (const run of incomplete) {\n const filePath = path.join('.resumable-workflow', `${run.runId}.json`);\n await fs.unlink(filePath).catch(() => {});\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":["fs","path"]}
|
|
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 // Auto-cleanup if enabled and successful\n if (this.config.autoCleanup) {\n await this.storage.deleteRun(currentState.runId);\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 * Manually clears all completed workflow runs for this workflow ID from storage.\n */\n async clearCompleted(): Promise<void> {\n const completed = await this.storage.listCompletedRuns(this.workflowId);\n await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));\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}","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 listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'pending');\n }\n\n listCompletedRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'completed');\n }\n\n private async listRuns(\n workflowId: string,\n status: 'pending' | 'completed'\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 try {\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 === status) {\n runs.push(run);\n }\n } catch (_e) {\n // Ignore corrupted files\n }\n }\n }\n return runs;\n } catch (_e) {\n return [];\n }\n }\n\n async deleteRun(runId: string): Promise<void> {\n try {\n await fs.unlink(this.getFilePath(runId));\n } catch (_e) {\n // Ignore if file doesn't exist\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resumable-workflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|