@asaidimu/utils-pipeline 1.0.2 → 1.0.4
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 +117 -276
- package/index.d.mts +419 -39
- package/index.d.ts +419 -39
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,328 +1,169 @@
|
|
|
1
|
-
|
|
1
|
+
# @asaidimu/utils-pipeline
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A production-grade, asynchronous, type-safe pipeline engine featuring conditional routing, checkpoint-based pause/resume mechanisms, concurrent execution, and atomic state transactions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](package.json)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Features
|
|
8
9
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
10
|
+
- **Type-safe State Management**: Unified store with atomic updates across steps and stages.
|
|
11
|
+
- **Conditional Routing**: Dynamically jump between stages, terminate early, or suspend execution.
|
|
12
|
+
- **Pause & Resume**: Suspend a pipeline at any stage, persist its state and artifacts, and resume later—even after a process restart.
|
|
13
|
+
- **Concurrent Execution**: Parallelize steps within a stage or run multiple sub-pipelines simultaneously.
|
|
14
|
+
- **Atomic Transactions**: State patches are committed atomically at stage boundaries, ensuring consistency.
|
|
15
|
+
- **Deep Observability**: Lifecycle events for every pipeline, stage, and step with full ancestry paths.
|
|
16
|
+
- **Abort Support**: Built-in support for `AbortSignal` to cancel long-running operations.
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
## Installation
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **Comprehensive lifecycle events** for monitoring
|
|
21
|
-
|
|
22
|
-
### Core Concepts
|
|
23
|
-
|
|
24
|
-
#### StageCarry
|
|
25
|
-
|
|
26
|
-
Data flows through the pipeline as a record mapping step keys to their outputs:
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
type StageCarry = Record<string, any>;
|
|
30
|
-
// Example: { fetch: { userId: 123 }, enrich: { ... }, save: { saved: true } }
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
#### PipelineStep
|
|
34
|
-
|
|
35
|
-
A single unit of work with three properties:
|
|
36
|
-
|
|
37
|
-
- `key` - Unique identifier (used as the output key in StageCarry)
|
|
38
|
-
- `order` - Stage number (steps with same order run in parallel)
|
|
39
|
-
- `action` - Async function receiving current carry and returning `Result<T>`
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
interface PipelineStep<TIn extends StageCarry = StageCarry, TOut = any> {
|
|
43
|
-
key: string;
|
|
44
|
-
order: number;
|
|
45
|
-
action: (input: TIn, signal?: AbortSignal) => Promise<Result<TOut>>;
|
|
46
|
-
}
|
|
20
|
+
```bash
|
|
21
|
+
bun add @asaidimu/utils-pipeline
|
|
22
|
+
# or
|
|
23
|
+
npm install @asaidimu/utils-pipeline
|
|
47
24
|
```
|
|
48
25
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
All pipeline actions must return a `Result` to distinguish success from failure:
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
type Result<T, E = SystemError> =
|
|
55
|
-
| { ok: true; value: T }
|
|
56
|
-
| { ok: false; error: E };
|
|
57
|
-
```
|
|
26
|
+
## Routing Sequential Pipeline (RSP)
|
|
58
27
|
|
|
59
|
-
|
|
28
|
+
The `Routing Sequential Pipeline` is the primary engine for complex, stateful workflows.
|
|
60
29
|
|
|
61
|
-
|
|
30
|
+
### Basic Usage
|
|
62
31
|
|
|
63
32
|
```typescript
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
key: "store",
|
|
69
|
-
action: async ({ validate }) => Result.ok(db.insert(validate)),
|
|
70
|
-
},
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
const result = await pipeline.execute("parse", '{"id":1}');
|
|
74
|
-
// result.value = { store: insertResult }
|
|
75
|
-
```
|
|
33
|
+
import {
|
|
34
|
+
PipelineFactory,
|
|
35
|
+
type RoutingPipelineDefinition,
|
|
36
|
+
} from "@asaidimu/utils-pipeline";
|
|
76
37
|
|
|
77
|
-
|
|
38
|
+
interface MyState {
|
|
39
|
+
counter: number;
|
|
40
|
+
}
|
|
78
41
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
[
|
|
42
|
+
const definition: RoutingPipelineDefinition<MyState> = {
|
|
43
|
+
id: "my-pipeline",
|
|
44
|
+
label: "My Business Process",
|
|
45
|
+
stages: [
|
|
83
46
|
{
|
|
84
|
-
|
|
85
|
-
|
|
47
|
+
id: "init",
|
|
48
|
+
order: 1,
|
|
49
|
+
label: "Initialization",
|
|
50
|
+
steps: {
|
|
51
|
+
setup: {
|
|
52
|
+
id: "setup",
|
|
53
|
+
label: "Setup Data",
|
|
54
|
+
action: async (ctx) => ({ counter: 1 }),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
86
57
|
},
|
|
87
58
|
{
|
|
88
|
-
|
|
89
|
-
|
|
59
|
+
id: "process",
|
|
60
|
+
order: 2,
|
|
61
|
+
label: "Main Processing",
|
|
62
|
+
steps: {
|
|
63
|
+
work: {
|
|
64
|
+
id: "work",
|
|
65
|
+
label: "Do Work",
|
|
66
|
+
action: async (ctx) => {
|
|
67
|
+
const counter = await ctx.use((c) => c.select((s) => s.counter));
|
|
68
|
+
return { counter: state.counter + 1 };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
// Conditional routing after this stage completes
|
|
73
|
+
router: (state) => (state.counter > 5 ? "end" : "process"),
|
|
90
74
|
},
|
|
91
75
|
{
|
|
92
|
-
|
|
93
|
-
|
|
76
|
+
id: "end",
|
|
77
|
+
order: 3,
|
|
78
|
+
label: "Finalize",
|
|
79
|
+
steps: {
|
|
80
|
+
cleanup: {
|
|
81
|
+
id: "cleanup",
|
|
82
|
+
label: "Cleanup",
|
|
83
|
+
action: async () => ({}),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
94
86
|
},
|
|
95
87
|
],
|
|
96
|
-
|
|
97
|
-
// cache, notify, and log execute simultaneously after fetch completes
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
#### Starting Mid-Pipeline
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// Skip initial stages, start from "process" step
|
|
104
|
-
const result = await pipeline.execute("process", { data: "pre-processed" });
|
|
105
|
-
// Only executes stages with order >= process.order
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
#### Deduplication (Single-Flight)
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// Three identical calls execute concurrently but only run the action once
|
|
112
|
-
const [a, b, c] = await Promise.all([
|
|
113
|
-
pipeline.execute("expensive", "user-123"),
|
|
114
|
-
pipeline.execute("expensive", "user-123"),
|
|
115
|
-
pipeline.execute("expensive", "user-123"),
|
|
116
|
-
]);
|
|
117
|
-
// a, b, c receive the same result; action called exactly once
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
#### Cancellation & Abort Signals
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
const controller = new AbortController();
|
|
124
|
-
|
|
125
|
-
// Start execution with abort capability
|
|
126
|
-
const executionPromise = pipeline.execute("long-running", params, {
|
|
127
|
-
signal: controller.signal,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Cancel after 1 second
|
|
131
|
-
setTimeout(() => controller.abort(), 1000);
|
|
132
|
-
|
|
133
|
-
const result = await executionPromise;
|
|
134
|
-
// result.ok === false with error.code === "CANCELLED"
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
#### Event Monitoring
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
// Global events for monitoring
|
|
141
|
-
pipeline.on("step:success", ({ stepKey, executionId, result }) => {
|
|
142
|
-
console.log(`[${executionId}] ${stepKey} completed`, result);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
pipeline.on("failure", ({ stepKey, error }) => {
|
|
146
|
-
console.error(`Pipeline failed at ${stepKey}:`, error.message);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Scoped events for a specific run
|
|
150
|
-
const ctx = pipeline.prepare("step", params);
|
|
151
|
-
ctx.on("step:success", (payload) => {
|
|
152
|
-
// Only triggered for this execution
|
|
153
|
-
});
|
|
154
|
-
await ctx.run();
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
#### Manual Context Management
|
|
88
|
+
};
|
|
158
89
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"process",
|
|
163
|
-
{ data: "input" },
|
|
164
|
-
{ signal: externalAbortSignal }, // optional
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// Add scoped listeners
|
|
168
|
-
context.on("stage:success", ({ order, carry }) => {
|
|
169
|
-
console.log(`Stage ${order} completed`);
|
|
90
|
+
const factory = new PipelineFactory(definition, {
|
|
91
|
+
storeFactory: async (runId) => createStore(runId), // Return a DataStore instance
|
|
92
|
+
initialStateFactory: () => ({ counter: 0 }),
|
|
170
93
|
});
|
|
171
94
|
|
|
172
|
-
//
|
|
95
|
+
// Prepare and run
|
|
96
|
+
const context = await factory.prepare();
|
|
173
97
|
const result = await context.run();
|
|
174
98
|
|
|
175
|
-
|
|
176
|
-
|
|
99
|
+
if (result.ok && result.value.status === "succeeded") {
|
|
100
|
+
console.log("Pipeline finished:", result.value.finalState);
|
|
101
|
+
}
|
|
177
102
|
```
|
|
178
103
|
|
|
179
|
-
###
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
const pipeline = new Pipeline();
|
|
183
|
-
|
|
184
|
-
// Add steps at runtime
|
|
185
|
-
pipeline
|
|
186
|
-
.step({ key: "a", order: 0, action: ok("first") })
|
|
187
|
-
.step({ key: "b", order: 1, action: ok("second") })
|
|
188
|
-
.step({ key: "c", order: 1, action: ok("parallel with b") });
|
|
189
|
-
|
|
190
|
-
await pipeline.execute("a", null);
|
|
191
|
-
```
|
|
104
|
+
### Pause and Resume
|
|
192
105
|
|
|
193
|
-
|
|
106
|
+
A router can signal a `pause`, which suspends the pipeline and writes a checkpoint.
|
|
194
107
|
|
|
195
108
|
```typescript
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
new SystemError({
|
|
203
|
-
code: "VALIDATION_ERROR",
|
|
204
|
-
message: "Invalid data",
|
|
205
|
-
operation: "validate",
|
|
206
|
-
}),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return Result.ok(carry.data);
|
|
210
|
-
},
|
|
109
|
+
// Inside a stage definition:
|
|
110
|
+
router: (state) => {
|
|
111
|
+
if (state.needsApproval) {
|
|
112
|
+
return { pause: "processing-stage", timeout: 86400000 }; // 24h timeout
|
|
113
|
+
}
|
|
114
|
+
return undefined; // natural advance
|
|
211
115
|
};
|
|
212
116
|
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
},
|
|
220
|
-
};
|
|
117
|
+
// Later, to resume:
|
|
118
|
+
const result = await factory.resume(runId);
|
|
119
|
+
if (result.ok) {
|
|
120
|
+
const context = result.value;
|
|
121
|
+
await context.run();
|
|
122
|
+
}
|
|
221
123
|
```
|
|
222
124
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
| Event | Payload | When |
|
|
226
|
-
| --------------- | ---------------------------------- | ------------------------------------ |
|
|
227
|
-
| `start` | `{ stepKey, executionId }` | Before any steps execute |
|
|
228
|
-
| `step:success` | `{ stepKey, executionId, result }` | Each successful step |
|
|
229
|
-
| `step:failure` | `{ stepKey, executionId, error }` | Each failed step (even in parallel) |
|
|
230
|
-
| `stage:success` | `{ order, executionId, carry }` | After all steps in a stage complete |
|
|
231
|
-
| `terminate` | `{ carry, executionId }` | When pipeline completes successfully |
|
|
232
|
-
| `failure` | `{ stepKey, executionId, error }` | On first pipeline failure |
|
|
233
|
-
|
|
234
|
-
### API Reference
|
|
125
|
+
## Sequential Pipeline (Simple)
|
|
235
126
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
| Method | Description |
|
|
239
|
-
| ----------------------------------------------- | --------------------------------------------------------- |
|
|
240
|
-
| `constructor(definition?)` | Create pipeline with optional declarative definition |
|
|
241
|
-
| `step<TIn, TOut>(definition)` | Register or overwrite a step, returns `this` for chaining |
|
|
242
|
-
| `execute<TIn>(stepKey, params, options?)` | Execute pipeline starting from `stepKey` |
|
|
243
|
-
| `prepare<TIn>(stepKey, params, options?, bus?)` | Create isolated execution context without running |
|
|
244
|
-
| `on<K>(event, handler)` | Subscribe to global pipeline events |
|
|
245
|
-
| `destroy()` | Clear all steps, listeners, and deduplication cache |
|
|
246
|
-
|
|
247
|
-
#### `PipelineExecutionContext` Class
|
|
248
|
-
|
|
249
|
-
| Method | Description |
|
|
250
|
-
| ----------------------- | ---------------------------------------------------- |
|
|
251
|
-
| `run()` | Execute the pipeline from this context's entry point |
|
|
252
|
-
| `abort()` | Cancel this specific execution |
|
|
253
|
-
| `on<K>(event, handler)` | Subscribe to events scoped to this execution |
|
|
254
|
-
|
|
255
|
-
#### `Result` Utilities
|
|
127
|
+
For simpler, linear workflows without conditional routing or persistence, use the standard `Pipeline` class.
|
|
256
128
|
|
|
257
129
|
```typescript
|
|
258
|
-
|
|
259
|
-
Result.ok(value); // -> { ok: true, value }
|
|
260
|
-
|
|
261
|
-
// Failure
|
|
262
|
-
Result.fail(error); // -> { ok: false, error }
|
|
130
|
+
import { Pipeline, Result } from "@asaidimu/utils-pipeline";
|
|
263
131
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
### Pipeline Definition Format
|
|
270
|
-
|
|
271
|
-
The definition uses array position to determine stage order:
|
|
132
|
+
const pipeline = new Pipeline([
|
|
133
|
+
{ key: "step1", order: 0, action: async () => Result.ok("First") },
|
|
134
|
+
{ key: "step2", order: 1, action: async () => Result.ok("Second") },
|
|
135
|
+
]);
|
|
272
136
|
|
|
273
|
-
|
|
274
|
-
type PipelineDefinition = Array<
|
|
275
|
-
PipelineStepDefinition | PipelineStepDefinition[]
|
|
276
|
-
>;
|
|
277
|
-
|
|
278
|
-
// Index 0 = stage order 0
|
|
279
|
-
// Index 1 = stage order 1, etc.
|
|
280
|
-
|
|
281
|
-
// Flat = single step per stage
|
|
282
|
-
[{ key: "a" }, { key: "b" }, { key: "c" }][
|
|
283
|
-
// Nested = parallel steps at that stage
|
|
284
|
-
({ key: "a" }, [{ key: "b" }, { key: "c" }], { key: "d" })
|
|
285
|
-
];
|
|
286
|
-
// Stage 0: a
|
|
287
|
-
// Stage 1: b, c (parallel)
|
|
288
|
-
// Stage 2: d
|
|
137
|
+
const result = await pipeline.execute("step1", null);
|
|
289
138
|
```
|
|
290
139
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
1. **Keep steps focused** - Each step should do one thing and do it well
|
|
294
|
-
2. **Use descriptive keys** - Keys become property names in StageCarry
|
|
295
|
-
3. **Handle errors gracefully** - Return `Result.fail()` with meaningful error codes
|
|
296
|
-
4. **Leverage deduplication** - Identical concurrent executions are automatically deduplicated
|
|
297
|
-
5. **Monitor with events** - Use lifecycle events for logging, metrics, and debugging
|
|
298
|
-
6. **Abort signals for timeouts** - Pass AbortSignal to enable cancellation
|
|
299
|
-
7. **Avoid side effects between stages** - Only pass data through StageCarry
|
|
140
|
+
## API Reference
|
|
300
141
|
|
|
301
|
-
###
|
|
142
|
+
### `PipelineFactory`
|
|
302
143
|
|
|
303
|
-
|
|
144
|
+
- `prepare(entry?, runId?)`: Creates a new `RunContext`.
|
|
145
|
+
- `resume(runId)`: Reconstructs a `RunContext` from a persisted checkpoint.
|
|
304
146
|
|
|
305
|
-
|
|
306
|
-
import { Pipeline } from "@core/pipeline";
|
|
147
|
+
### `RunContext`
|
|
307
148
|
|
|
308
|
-
|
|
309
|
-
|
|
149
|
+
- `run()`: Starts or continues execution.
|
|
150
|
+
- `abort()`: Signals the pipeline to terminate at the next stage boundary.
|
|
151
|
+
- `on(event, handler)`: Subscribes to lifecycle events.
|
|
310
152
|
|
|
311
|
-
|
|
153
|
+
### Lifecycle Events
|
|
312
154
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
155
|
+
- `pipeline:start` / `pipeline:success` / `pipeline:failure` / `pipeline:paused`
|
|
156
|
+
- `stage:start` / `stage:success` / `stage:failure` / `stage:paused`
|
|
157
|
+
- `step:start` / `step:success` / `step:failure`
|
|
158
|
+
- `router:evaluated`
|
|
316
159
|
|
|
317
|
-
|
|
160
|
+
## Best Practices
|
|
318
161
|
|
|
319
|
-
|
|
320
|
-
|
|
162
|
+
1. **Atomic Steps**: Each step should perform a single, focused operation.
|
|
163
|
+
2. **Pure Store Interaction**: Steps should ideally only interact with the world via the provided store and context.
|
|
164
|
+
3. **Handle Cancellation**: Long-running steps should check `pcxt.signal` or pass it to underlying async calls.
|
|
165
|
+
4. **Descriptive Labels**: Use human-readable labels for stages and steps as they appear in event paths.
|
|
321
166
|
|
|
322
|
-
|
|
167
|
+
## License
|
|
323
168
|
|
|
324
|
-
|
|
325
|
-
- **API orchestration** (parallel requests with aggregation)
|
|
326
|
-
- **Build systems** (task graphs with dependencies)
|
|
327
|
-
- **Form validation** (parallel validation rules)
|
|
328
|
-
- **Background job processing** (staged workflows with cancellation)
|
|
169
|
+
MIT
|