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 +109 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +219 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +180 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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":"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","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
|
+
}
|