@asaidimu/utils-pipeline 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/LICENSE.md +21 -0
- package/README.md +328 -0
- package/index.d.mts +502 -0
- package/index.d.ts +502 -0
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/package.json +63 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saidimu
|
|
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
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
## Pipeline Utility
|
|
2
|
+
|
|
3
|
+
A staged, concurrent pipeline orchestration framework for complex async workflows with parallel execution, cancellation, and single-flight deduplication.
|
|
4
|
+
|
|
5
|
+
### Why Use This?
|
|
6
|
+
|
|
7
|
+
Traditional async orchestration often leads to:
|
|
8
|
+
|
|
9
|
+
- Nested callbacks and manual Promise.all coordination
|
|
10
|
+
- Race conditions in shared state
|
|
11
|
+
- Difficult cancellation across multiple operations
|
|
12
|
+
- Duplicate work from concurrent identical requests
|
|
13
|
+
|
|
14
|
+
This pipeline solves these by providing:
|
|
15
|
+
|
|
16
|
+
- **Declarative stages** with automatic parallelization
|
|
17
|
+
- **Isolated execution contexts** per run
|
|
18
|
+
- **Built-in deduplication** for identical concurrent executions
|
|
19
|
+
- **Scoped cancellation** via AbortController
|
|
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
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Result Type
|
|
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
|
+
```
|
|
58
|
+
|
|
59
|
+
### Usage Examples
|
|
60
|
+
|
|
61
|
+
#### Basic Sequential Pipeline
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const pipeline = new Pipeline([
|
|
65
|
+
{ key: "parse", action: async ({ parse }) => Result.ok(JSON.parse(parse)) },
|
|
66
|
+
{ key: "validate", action: async ({ parse }) => Result.ok(validate(parse)) },
|
|
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
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Parallel Processing
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const pipeline = new Pipeline([
|
|
81
|
+
{ key: "fetch", action: async () => Result.ok(await api.fetch()) },
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
key: "cache",
|
|
85
|
+
action: async ({ fetch }) => Result.ok(await redis.set(fetch)),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "notify",
|
|
89
|
+
action: async ({ fetch }) => Result.ok(await webhook.send(fetch)),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "log",
|
|
93
|
+
action: async ({ fetch }) => Result.ok(await logger.info(fetch)),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
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
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// For fine-grained control over concurrent runs
|
|
161
|
+
const context = pipeline.prepare(
|
|
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`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Execute with full control
|
|
173
|
+
const result = await context.run();
|
|
174
|
+
|
|
175
|
+
// Cancel individual run
|
|
176
|
+
context.abort();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Dynamic Step Registration
|
|
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
|
+
```
|
|
192
|
+
|
|
193
|
+
### Error Handling Patterns
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// Returning a failed Result stops the pipeline
|
|
197
|
+
const step = {
|
|
198
|
+
key: "validate",
|
|
199
|
+
action: async (carry) => {
|
|
200
|
+
if (!carry.data.valid) {
|
|
201
|
+
return Result.fail(
|
|
202
|
+
new SystemError({
|
|
203
|
+
code: "VALIDATION_ERROR",
|
|
204
|
+
message: "Invalid data",
|
|
205
|
+
operation: "validate",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return Result.ok(carry.data);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Throwing also works (automatically wrapped in SystemError)
|
|
214
|
+
const another = {
|
|
215
|
+
key: "risky",
|
|
216
|
+
action: async () => {
|
|
217
|
+
throw new Error("Something went wrong");
|
|
218
|
+
// Becomes SystemError with code INTERNAL_ERROR
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Lifecycle Events
|
|
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
|
|
235
|
+
|
|
236
|
+
#### `Pipeline` Class
|
|
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
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Success
|
|
259
|
+
Result.ok(value); // -> { ok: true, value }
|
|
260
|
+
|
|
261
|
+
// Failure
|
|
262
|
+
Result.fail(error); // -> { ok: false, error }
|
|
263
|
+
|
|
264
|
+
// From the @core/error package
|
|
265
|
+
Errors.internalError(original, message); // Creates SystemError
|
|
266
|
+
Errors.invalidCommand(message); // Creates INVALID_COMMAND error
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Pipeline Definition Format
|
|
270
|
+
|
|
271
|
+
The definition uses array position to determine stage order:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
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
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Best Practices
|
|
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
|
|
300
|
+
|
|
301
|
+
### Testing
|
|
302
|
+
|
|
303
|
+
The pipeline is designed for testability:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { Pipeline } from "@core/pipeline";
|
|
307
|
+
|
|
308
|
+
// Mock steps with controlled delays
|
|
309
|
+
const mockAction = vi.fn().mockResolvedValue(Result.ok("mocked"));
|
|
310
|
+
|
|
311
|
+
const pipeline = new Pipeline([{ key: "test", action: mockAction }]);
|
|
312
|
+
|
|
313
|
+
await pipeline.execute("test", null);
|
|
314
|
+
expect(mockAction).toHaveBeenCalled();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### See Also
|
|
318
|
+
|
|
319
|
+
- [`@core/error`](./error) - SystemError and Result types
|
|
320
|
+
- [`@core/events`](./events) - EventBus implementation
|
|
321
|
+
|
|
322
|
+
This utility is particularly valuable for:
|
|
323
|
+
|
|
324
|
+
- **Data processing pipelines** (ETL, image processing, document workflows)
|
|
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)
|