@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 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)