@asaidimu/utils-pipeline 1.0.2 → 1.0.3

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 CHANGED
@@ -1,328 +1,169 @@
1
- ## Pipeline Utility
1
+ # @asaidimu/utils-pipeline
2
2
 
3
- A staged, concurrent pipeline orchestration framework for complex async workflows with parallel execution, cancellation, and single-flight deduplication.
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
- ### Why Use This?
5
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Version](https://img.shields.io/badge/version-1.0.0-green.svg)](package.json)
6
7
 
7
- Traditional async orchestration often leads to:
8
+ ## Features
8
9
 
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
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
- This pipeline solves these by providing:
18
+ ## Installation
15
19
 
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
- }
20
+ ```bash
21
+ bun add @asaidimu/utils-pipeline
22
+ # or
23
+ npm install @asaidimu/utils-pipeline
47
24
  ```
48
25
 
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
- ```
26
+ ## Routing Sequential Pipeline (RSP)
58
27
 
59
- ### Usage Examples
28
+ The `Routing Sequential Pipeline` is the primary engine for complex, stateful workflows.
60
29
 
61
- #### Basic Sequential Pipeline
30
+ ### Basic Usage
62
31
 
63
32
  ```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
- ```
33
+ import {
34
+ PipelineFactory,
35
+ type RoutingPipelineDefinition,
36
+ } from "@asaidimu/utils-pipeline";
76
37
 
77
- #### Parallel Processing
38
+ interface MyState {
39
+ counter: number;
40
+ }
78
41
 
79
- ```typescript
80
- const pipeline = new Pipeline([
81
- { key: "fetch", action: async () => Result.ok(await api.fetch()) },
82
- [
42
+ const definition: RoutingPipelineDefinition<MyState> = {
43
+ id: "my-pipeline",
44
+ label: "My Business Process",
45
+ stages: [
83
46
  {
84
- key: "cache",
85
- action: async ({ fetch }) => Result.ok(await redis.set(fetch)),
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
- key: "notify",
89
- action: async ({ fetch }) => Result.ok(await webhook.send(fetch)),
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
- key: "log",
93
- action: async ({ fetch }) => Result.ok(await logger.info(fetch)),
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
- ```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`);
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
- // Execute with full control
95
+ // Prepare and run
96
+ const context = await factory.prepare();
173
97
  const result = await context.run();
174
98
 
175
- // Cancel individual run
176
- context.abort();
99
+ if (result.ok && result.value.status === "succeeded") {
100
+ console.log("Pipeline finished:", result.value.finalState);
101
+ }
177
102
  ```
178
103
 
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
- ```
104
+ ### Pause and Resume
192
105
 
193
- ### Error Handling Patterns
106
+ A router can signal a `pause`, which suspends the pipeline and writes a checkpoint.
194
107
 
195
108
  ```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
- },
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
- // 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
- };
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
- ### 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
125
+ ## Sequential Pipeline (Simple)
235
126
 
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
127
+ For simpler, linear workflows without conditional routing or persistence, use the standard `Pipeline` class.
256
128
 
257
129
  ```typescript
258
- // Success
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
- // 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:
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
- ```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
137
+ const result = await pipeline.execute("step1", null);
289
138
  ```
290
139
 
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
140
+ ## API Reference
300
141
 
301
- ### Testing
142
+ ### `PipelineFactory`
302
143
 
303
- The pipeline is designed for testability:
144
+ - `prepare(entry?, runId?)`: Creates a new `RunContext`.
145
+ - `resume(runId)`: Reconstructs a `RunContext` from a persisted checkpoint.
304
146
 
305
- ```typescript
306
- import { Pipeline } from "@core/pipeline";
147
+ ### `RunContext`
307
148
 
308
- // Mock steps with controlled delays
309
- const mockAction = vi.fn().mockResolvedValue(Result.ok("mocked"));
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
- const pipeline = new Pipeline([{ key: "test", action: mockAction }]);
153
+ ### Lifecycle Events
312
154
 
313
- await pipeline.execute("test", null);
314
- expect(mockAction).toHaveBeenCalled();
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
- ### See Also
160
+ ## Best Practices
318
161
 
319
- - [`@core/error`](./error) - SystemError and Result types
320
- - [`@core/events`](./events) - EventBus implementation
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
- This utility is particularly valuable for:
167
+ ## License
323
168
 
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)
169
+ MIT