rest-pipeline-js 1.3.9 → 1.3.11
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 +416 -330
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,175 +4,183 @@
|
|
|
4
4
|
</h1>
|
|
5
5
|
<img
|
|
6
6
|
src="https://s3.twcstorage.ru/c9a2cc89-780f97fd-311d-4a1a-b86f-c25665c9dc46/images/npm/rest-pipeline-js.webp"
|
|
7
|
-
alt="
|
|
7
|
+
alt="rest-pipeline-js"
|
|
8
8
|
style="max-width:100%;width:auto;height:300px;border-radius:12px"
|
|
9
9
|
/>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Flexible, modular pipeline orchestrator for REST APIs — sequential and parallel stages, retry with backoff, response caching, rate limiting, auth provider, stream stages (SSE / AsyncIterable), plugin system, and Vue / React integrations — all with a single dependency (axios).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Demo](#demo)
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [createRestClient](#createrestclient)
|
|
23
|
+
- [Auth Provider](#auth-provider)
|
|
24
|
+
- [Log Sanitization](#log-sanitization)
|
|
25
|
+
- [RequestExecutor](#requestexecutor)
|
|
26
|
+
- [PipelineOrchestrator](#pipelineorchestrator)
|
|
27
|
+
- [Parallel stages](#parallel-stages)
|
|
28
|
+
- [Global middleware](#global-middleware)
|
|
29
|
+
- [Pause / Resume](#pause--resume)
|
|
30
|
+
- [Export / Import state](#export--import-state)
|
|
31
|
+
- [Pipeline metrics](#pipeline-metrics)
|
|
32
|
+
- [createPipeline() + pipe() builder](#createpipeline--pipe-builder)
|
|
33
|
+
- [validatePipelineConfig()](#validatepipelineconfig)
|
|
34
|
+
- [Plugin system](#plugin-system)
|
|
35
|
+
- [Persist adapter](#persist-adapter)
|
|
36
|
+
- [Stream stages](#stream-stages-sse--asynciterable)
|
|
37
|
+
- [HTTP Adapter](#http-adapter-custom-fetch--edge-environments)
|
|
38
|
+
- [Vue integration](#vue-integration)
|
|
39
|
+
- [React integration](#react-integration)
|
|
40
|
+
- [Entry points](#entry-points)
|
|
41
|
+
- [Architecture](#architecture)
|
|
42
|
+
- [Bundle size & peer dependencies](#bundle-size--peer-dependencies)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **`createRestClient()`** — full-featured HTTP client built on top of axios: retry with exponential backoff and `Retry-After` support, response caching for GET requests, rate limiting (concurrency + req/interval), auth provider with automatic 401 refresh, request cancellation by key, custom HTTP adapters
|
|
49
|
+
- **`PipelineOrchestrator`** — sequential and parallel stage execution; each stage has `condition`, `before`, `request`, `after`, `errorHandler` hooks; `sharedData` pool shared across all stages
|
|
50
|
+
- **Global middleware** — `beforeEach` / `afterEach` / `onError` hooks that apply to every stage without modifying individual configs
|
|
51
|
+
- **Parallel groups** — multiple stages run concurrently via `Promise.all`; single failure stops the group
|
|
52
|
+
- **Pause / Resume / Abort** — `pause()` waits after the current stage; `resume()` continues; `abort()` cancels the current HTTP request via `AbortController`
|
|
53
|
+
- **Export / Import state** — serialize `stageResults` + logs to a plain object; restore on the next page load
|
|
54
|
+
- **Stream stages** — `stream: async function*` for SSE / any `AsyncIterable`; `onChunk` callback in real time; abort-aware
|
|
55
|
+
- **Pipeline metrics** — `onPipelineStart`, `onPipelineEnd`, `onStepDuration` callbacks without touching stage logic
|
|
56
|
+
- **`createPipeline()` / `pipe()` builder** — short factory and fluent builder API for common patterns
|
|
57
|
+
- **`validatePipelineConfig()`** — catch duplicate keys, empty keys, type errors before runtime
|
|
58
|
+
- **Plugin system** — install reusable behavior (logging, analytics, etc.); cleanup via `destroy()`
|
|
59
|
+
- **Persist adapter** — pluggable save/load interface; auto-save after each stage
|
|
60
|
+
- **Log sanitization** — mask sensitive headers (`authorization`, `x-api-key`, `cookie`, …) in metrics callbacks
|
|
61
|
+
- **Vue integration** — `usePipelineRunVue`, `usePipelineProgressVue`, and more (import from `rest-pipeline-js/vue`)
|
|
62
|
+
- **React integration** — `usePipelineRunReact`, `usePipelineProgressReact`, and more (import from `rest-pipeline-js/react`)
|
|
63
|
+
- **Tree-shakeable** — `sideEffects: false`; Vue and React entry points are code-split
|
|
13
64
|
|
|
14
65
|
---
|
|
15
66
|
|
|
16
67
|
## Installation
|
|
17
68
|
|
|
18
|
-
```
|
|
19
|
-
npm
|
|
69
|
+
```bash
|
|
70
|
+
npm install rest-pipeline-js
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Peer dependencies for framework integrations:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Vue
|
|
77
|
+
npm install vue@>=3.3
|
|
78
|
+
|
|
79
|
+
# React
|
|
80
|
+
npm install react@>=19 react-dom@>=19
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Demo
|
|
86
|
+
|
|
87
|
+
A live interactive demo of the pipeline running against a real flight-search API — 4 sequential stages: airport lookup, availability, ancillary services, and seat map.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/macrulezru/pipeline-js.git
|
|
91
|
+
cd pipeline-js
|
|
92
|
+
npm install
|
|
93
|
+
npm run demo:vue
|
|
20
94
|
```
|
|
21
95
|
|
|
22
|
-
|
|
96
|
+
Opens at `http://localhost:3000` (or the next available port). Click **Run Pipeline** to execute all stages and watch results appear in real time. A boarding pass is rendered when all stages succeed.
|
|
23
97
|
|
|
24
|
-
|
|
98
|
+
---
|
|
25
99
|
|
|
26
|
-
|
|
100
|
+
## Quick start
|
|
27
101
|
|
|
28
102
|
```js
|
|
29
|
-
import { createRestClient } from "rest-pipeline-js";
|
|
103
|
+
import { createRestClient, PipelineOrchestrator } from "rest-pipeline-js";
|
|
30
104
|
|
|
105
|
+
// 1. Create a REST client
|
|
31
106
|
const client = createRestClient({
|
|
32
107
|
baseURL: "https://api.example.com",
|
|
33
|
-
|
|
34
|
-
retry: {
|
|
35
|
-
attempts: 2,
|
|
36
|
-
delayMs: 500,
|
|
37
|
-
backoffMultiplier: 2,
|
|
38
|
-
retriableStatus: [429, 500, 503],
|
|
39
|
-
},
|
|
108
|
+
retry: { attempts: 2, delayMs: 500, backoffMultiplier: 2 },
|
|
40
109
|
cache: { enabled: true, ttlMs: 60000 },
|
|
41
|
-
rateLimit: { maxConcurrent: 3, maxRequestsPerInterval: 10, intervalMs: 1000 },
|
|
42
|
-
// Auth Provider — token injected automatically, 401 triggers refresh + one retry
|
|
43
110
|
auth: {
|
|
44
111
|
getToken: async () => localStorage.getItem("token") ?? "",
|
|
45
|
-
onUnauthorized: async () => {
|
|
46
|
-
/* refresh token here */
|
|
47
|
-
},
|
|
112
|
+
onUnauthorized: async () => { /* refresh token */ },
|
|
48
113
|
},
|
|
49
|
-
// Mask sensitive headers in metrics (authorization, x-api-key, cookie, …)
|
|
50
|
-
sanitizeHeaders: true,
|
|
51
114
|
});
|
|
52
115
|
|
|
53
116
|
const res = await client.get("/users/1");
|
|
54
|
-
console.log(res.data);
|
|
55
|
-
|
|
56
|
-
// PATCH support
|
|
57
|
-
await client.patch("/users/1", { name: "Alice" });
|
|
58
|
-
|
|
59
|
-
// Cancellable request
|
|
60
|
-
const req = client.cancellableRequest("my-key", "/search", {
|
|
61
|
-
params: { q: "foo" },
|
|
62
|
-
});
|
|
63
|
-
// Cancel it any time:
|
|
64
|
-
client.cancelRequest("my-key");
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
#### Example: Run a pipeline, handle errors, track progress, use shared data
|
|
70
|
-
|
|
71
|
-
```js
|
|
72
|
-
import { PipelineOrchestrator } from "rest-pipeline-js";
|
|
73
117
|
|
|
118
|
+
// 2. Run a pipeline
|
|
74
119
|
const orchestrator = new PipelineOrchestrator({
|
|
75
120
|
config: {
|
|
76
121
|
stages: [
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
request: async ({ sharedData }) => {
|
|
80
|
-
const res = await fetch(`/api/users/${sharedData.userId}`);
|
|
81
|
-
return res.json();
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
key: "processData",
|
|
86
|
-
condition: ({ prev }) => prev !== null,
|
|
87
|
-
before: ({ prev }) => ({ ...prev, processed: true }),
|
|
88
|
-
request: async ({ prev }) => prev,
|
|
89
|
-
after: ({ result }) => ({ ...result, finishedAt: Date.now() }),
|
|
90
|
-
},
|
|
122
|
+
{ key: "fetchUser", request: async ({ sharedData }) => client.get(`/users/${sharedData.userId}`) },
|
|
123
|
+
{ key: "processData", request: async ({ prev }) => ({ ...prev.data, processed: true }) },
|
|
91
124
|
],
|
|
92
|
-
middleware: {
|
|
93
|
-
beforeEach: ({ stage }) => console.log("Starting:", stage.key),
|
|
94
|
-
afterEach: ({ stage, result }) =>
|
|
95
|
-
console.log("Done:", stage.key, result.data),
|
|
96
|
-
onError: ({ stage, error }) =>
|
|
97
|
-
console.error("Error in", stage.key, error),
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
httpConfig: {
|
|
101
|
-
baseURL: "https://api.example.com",
|
|
102
|
-
retry: { attempts: 2, delayMs: 1000, backoffMultiplier: 2 },
|
|
103
|
-
cache: { enabled: true, ttlMs: 60000 },
|
|
104
125
|
},
|
|
105
126
|
sharedData: { userId: 42 },
|
|
106
|
-
options: { autoReset: true },
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
orchestrator.subscribeProgress((progress) => {
|
|
110
|
-
console.log(
|
|
111
|
-
"Stage:",
|
|
112
|
-
progress.currentStage,
|
|
113
|
-
"Statuses:",
|
|
114
|
-
progress.stageStatuses,
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
orchestrator.on("step:fetchUser:success", (payload) => {
|
|
119
|
-
console.log("fetchUser done:", payload.data);
|
|
120
127
|
});
|
|
121
128
|
|
|
122
129
|
const result = await orchestrator.run();
|
|
123
|
-
console.log(
|
|
124
|
-
console.log("Stage results:", result.stageResults);
|
|
130
|
+
console.log(result.success, result.stageResults);
|
|
125
131
|
```
|
|
126
132
|
|
|
127
133
|
---
|
|
128
134
|
|
|
129
|
-
|
|
135
|
+
## createRestClient
|
|
130
136
|
|
|
131
|
-
|
|
137
|
+
```ts
|
|
138
|
+
createRestClient(config: HttpConfig): RestClient
|
|
139
|
+
```
|
|
132
140
|
|
|
133
141
|
Creates a REST client with advanced HTTP features.
|
|
134
142
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| Method
|
|
138
|
-
|
|
139
|
-
| `get(url, config?)`
|
|
140
|
-
| `post(url, data?, config?)`
|
|
141
|
-
| `put(url, data?, config?)`
|
|
142
|
-
| `patch(url, data?, config?)`
|
|
143
|
-
| `delete(url, config?)`
|
|
144
|
-
| `request(url, config?)`
|
|
145
|
-
| `cancellableRequest(key, url, config?)` | Request cancellable by key
|
|
146
|
-
| `cancelRequest(key)`
|
|
147
|
-
| `clearCache()`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
| Option
|
|
152
|
-
|
|
153
|
-
| `baseURL`
|
|
154
|
-
| `timeout`
|
|
155
|
-
| `headers`
|
|
156
|
-
| `withCredentials`
|
|
157
|
-
| `retry.attempts`
|
|
158
|
-
| `retry.delayMs`
|
|
159
|
-
| `retry.backoffMultiplier`
|
|
160
|
-
| `retry.retriableStatus`
|
|
161
|
-
| `
|
|
162
|
-
| `cache.
|
|
163
|
-
| `
|
|
164
|
-
| `rateLimit.
|
|
165
|
-
| `rateLimit.
|
|
166
|
-
| `
|
|
167
|
-
| `metrics.
|
|
168
|
-
| `
|
|
169
|
-
| `auth.
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `adapter`
|
|
174
|
-
|
|
175
|
-
|
|
143
|
+
### Methods
|
|
144
|
+
|
|
145
|
+
| Method | Description |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `get(url, config?)` | GET request |
|
|
148
|
+
| `post(url, data?, config?)` | POST request |
|
|
149
|
+
| `put(url, data?, config?)` | PUT request |
|
|
150
|
+
| `patch(url, data?, config?)` | PATCH request |
|
|
151
|
+
| `delete(url, config?)` | DELETE request |
|
|
152
|
+
| `request(url, config?)` | Generic request |
|
|
153
|
+
| `cancellableRequest(key, url, config?)` | Request cancellable by key |
|
|
154
|
+
| `cancelRequest(key)` | Cancel request by key |
|
|
155
|
+
| `clearCache()` | Clear this client's response cache |
|
|
156
|
+
|
|
157
|
+
### HttpConfig options
|
|
158
|
+
|
|
159
|
+
| Option | Description |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `baseURL` | Base URL for all requests |
|
|
162
|
+
| `timeout` | Request timeout in ms |
|
|
163
|
+
| `headers` | Default headers |
|
|
164
|
+
| `withCredentials` | Include cookies |
|
|
165
|
+
| `retry.attempts` | Number of retry attempts |
|
|
166
|
+
| `retry.delayMs` | Base delay between retries in ms |
|
|
167
|
+
| `retry.backoffMultiplier` | Exponential backoff multiplier |
|
|
168
|
+
| `retry.retriableStatus` | HTTP status codes eligible for retry (e.g. `[429, 500, 503]`) |
|
|
169
|
+
| `retry.maxRetryAfterMs` | Max wait from `Retry-After` header in ms (default: `60000`) |
|
|
170
|
+
| `cache.enabled` | Enable response caching for GET requests |
|
|
171
|
+
| `cache.ttlMs` | Cache TTL in ms |
|
|
172
|
+
| `rateLimit.maxConcurrent` | Max simultaneous requests |
|
|
173
|
+
| `rateLimit.maxRequestsPerInterval` | Max requests per time window |
|
|
174
|
+
| `rateLimit.intervalMs` | Time window size in ms |
|
|
175
|
+
| `metrics.onRequestStart` | Callback on request start |
|
|
176
|
+
| `metrics.onRequestEnd` | Callback on request end (includes duration and bytes) |
|
|
177
|
+
| `auth.getToken` | Async function returning a Bearer token (called before every request) |
|
|
178
|
+
| `auth.onUnauthorized` | Optional async callback on 401 — refresh the token here; request is retried once |
|
|
179
|
+
| `sanitizeHeaders` | Mask sensitive headers in metrics callbacks (default: `false`) |
|
|
180
|
+
| `sensitiveHeaders` | Additional headers to mask (extends `DEFAULT_SENSITIVE_HEADERS`) |
|
|
181
|
+
| `adapter` | Custom HTTP adapter (e.g. native `fetch`) — replaces built-in axios |
|
|
182
|
+
|
|
183
|
+
### Per-request cache override
|
|
176
184
|
|
|
177
185
|
```js
|
|
178
186
|
const res = await client.get("/data", {
|
|
@@ -182,9 +190,44 @@ const res = await client.get("/data", {
|
|
|
182
190
|
});
|
|
183
191
|
```
|
|
184
192
|
|
|
193
|
+
### Full example
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
import { createRestClient } from "rest-pipeline-js";
|
|
197
|
+
|
|
198
|
+
const client = createRestClient({
|
|
199
|
+
baseURL: "https://api.example.com",
|
|
200
|
+
timeout: 5000,
|
|
201
|
+
retry: {
|
|
202
|
+
attempts: 2,
|
|
203
|
+
delayMs: 500,
|
|
204
|
+
backoffMultiplier: 2,
|
|
205
|
+
retriableStatus: [429, 500, 503],
|
|
206
|
+
},
|
|
207
|
+
cache: { enabled: true, ttlMs: 60000 },
|
|
208
|
+
rateLimit: { maxConcurrent: 3, maxRequestsPerInterval: 10, intervalMs: 1000 },
|
|
209
|
+
auth: {
|
|
210
|
+
getToken: async () => localStorage.getItem("token") ?? "",
|
|
211
|
+
onUnauthorized: async () => { /* refresh token here */ },
|
|
212
|
+
},
|
|
213
|
+
sanitizeHeaders: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const res = await client.get("/users/1");
|
|
217
|
+
console.log(res.data);
|
|
218
|
+
|
|
219
|
+
// PATCH support
|
|
220
|
+
await client.patch("/users/1", { name: "Alice" });
|
|
221
|
+
|
|
222
|
+
// Cancellable request
|
|
223
|
+
const req = client.cancellableRequest("my-key", "/search", { params: { q: "foo" } });
|
|
224
|
+
// Cancel it any time:
|
|
225
|
+
client.cancelRequest("my-key");
|
|
226
|
+
```
|
|
227
|
+
|
|
185
228
|
---
|
|
186
229
|
|
|
187
|
-
|
|
230
|
+
## Auth Provider
|
|
188
231
|
|
|
189
232
|
Automatically inject an `Authorization: Bearer <token>` header before every request. On a `401` response, `onUnauthorized` is called (e.g. to refresh the token) and the request is retried **once** — preventing infinite loops.
|
|
190
233
|
|
|
@@ -196,7 +239,6 @@ const client = createRestClient({
|
|
|
196
239
|
return localStorage.getItem("access_token") ?? "";
|
|
197
240
|
},
|
|
198
241
|
onUnauthorized: async () => {
|
|
199
|
-
// refresh token, update storage
|
|
200
242
|
const newToken = await refreshAccessToken();
|
|
201
243
|
localStorage.setItem("access_token", newToken);
|
|
202
244
|
},
|
|
@@ -209,7 +251,7 @@ const res = await client.get("/profile");
|
|
|
209
251
|
|
|
210
252
|
---
|
|
211
253
|
|
|
212
|
-
|
|
254
|
+
## Log Sanitization
|
|
213
255
|
|
|
214
256
|
Mask sensitive headers in metrics callbacks (`onRequestStart` / `onRequestEnd`) so they never appear in logs.
|
|
215
257
|
|
|
@@ -221,8 +263,8 @@ import { createRestClient, DEFAULT_SENSITIVE_HEADERS } from "rest-pipeline-js";
|
|
|
221
263
|
|
|
222
264
|
const client = createRestClient({
|
|
223
265
|
baseURL: "https://api.example.com",
|
|
224
|
-
sanitizeHeaders: true,
|
|
225
|
-
sensitiveHeaders: ["x-internal-secret"],
|
|
266
|
+
sanitizeHeaders: true, // opt-in — disabled by default
|
|
267
|
+
sensitiveHeaders: ["x-internal-secret"], // extend the default list
|
|
226
268
|
metrics: {
|
|
227
269
|
onRequestStart: (info) => {
|
|
228
270
|
// info.requestHeaders — sensitive values replaced with "REDACTED"
|
|
@@ -232,7 +274,7 @@ const client = createRestClient({
|
|
|
232
274
|
});
|
|
233
275
|
```
|
|
234
276
|
|
|
235
|
-
|
|
277
|
+
Use `sanitizeHeadersMap` directly:
|
|
236
278
|
|
|
237
279
|
```js
|
|
238
280
|
import { sanitizeHeadersMap } from "rest-pipeline-js";
|
|
@@ -246,9 +288,9 @@ const safe = sanitizeHeadersMap(
|
|
|
246
288
|
|
|
247
289
|
---
|
|
248
290
|
|
|
249
|
-
|
|
291
|
+
## RequestExecutor
|
|
250
292
|
|
|
251
|
-
Wrapper for REST requests with retry, timeout (via AbortController), Retry-After header support, and backoff.
|
|
293
|
+
Wrapper for REST requests with retry, timeout (via `AbortController`), `Retry-After` header support, and backoff.
|
|
252
294
|
|
|
253
295
|
```js
|
|
254
296
|
import { RequestExecutor } from "rest-pipeline-js";
|
|
@@ -260,7 +302,7 @@ const executor = new RequestExecutor({
|
|
|
260
302
|
delayMs: 500,
|
|
261
303
|
backoffMultiplier: 2,
|
|
262
304
|
retriableStatus: [429, 500, 502, 503],
|
|
263
|
-
maxRetryAfterMs: 30000,
|
|
305
|
+
maxRetryAfterMs: 30000, // cap Retry-After at 30 s
|
|
264
306
|
},
|
|
265
307
|
});
|
|
266
308
|
|
|
@@ -272,11 +314,11 @@ When the server returns a `Retry-After` header (numeric seconds or HTTP-date), t
|
|
|
272
314
|
|
|
273
315
|
---
|
|
274
316
|
|
|
275
|
-
|
|
317
|
+
## PipelineOrchestrator
|
|
276
318
|
|
|
277
319
|
Main class for building and managing a pipeline of sequential (and parallel) stages.
|
|
278
320
|
|
|
279
|
-
|
|
321
|
+
### Constructor
|
|
280
322
|
|
|
281
323
|
```js
|
|
282
324
|
new PipelineOrchestrator({
|
|
@@ -287,46 +329,46 @@ new PipelineOrchestrator({
|
|
|
287
329
|
})
|
|
288
330
|
```
|
|
289
331
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
| Method
|
|
293
|
-
|
|
294
|
-
| `run(onStepPause?, externalSignal?)`
|
|
295
|
-
| `rerunStep(stepKey, options?)`
|
|
296
|
-
| `abort()`
|
|
297
|
-
| `isAborted()`
|
|
298
|
-
| `pause()`
|
|
299
|
-
| `resume()`
|
|
300
|
-
| `isPaused()`
|
|
301
|
-
| `exportState()`
|
|
302
|
-
| `importState(state)`
|
|
303
|
-
| `getStageResults()`
|
|
304
|
-
| `destroy()`
|
|
305
|
-
| `subscribeProgress(listener)`
|
|
306
|
-
| `subscribeStageResults(listener)`
|
|
307
|
-
| `subscribeStepProgress(stepKey, listener)` | Subscribe to a specific stage's progress
|
|
308
|
-
| `on(eventName, handler)`
|
|
309
|
-
| `onStepStart/Finish/Error(handler)`
|
|
310
|
-
| `getProgress()`
|
|
311
|
-
| `getLogs()`
|
|
312
|
-
| `clearStageResults()`
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
| Parameter
|
|
317
|
-
|
|
318
|
-
| `key`
|
|
319
|
-
| `request({ prev, allResults, sharedData })`
|
|
320
|
-
| `condition({ prev, allResults, sharedData })` | If returns `false`, stage is skipped with status `"skipped"`
|
|
321
|
-
| `before({ prev, allResults, sharedData })`
|
|
322
|
-
| `after({ result, allResults, sharedData })`
|
|
323
|
-
| `errorHandler({ error, key, sharedData })`
|
|
324
|
-
| `retryCount`
|
|
325
|
-
| `timeoutMs`
|
|
326
|
-
| `pauseBefore`
|
|
327
|
-
| `pauseAfter`
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
### Methods
|
|
333
|
+
|
|
334
|
+
| Method | Description |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `run(onStepPause?, externalSignal?)` | Execute all stages. Returns `{ stageResults, success }` |
|
|
337
|
+
| `rerunStep(stepKey, options?)` | Re-execute a single stage (respects condition, before, after, middleware) |
|
|
338
|
+
| `abort()` | Abort pipeline execution (cancels the current HTTP request via AbortSignal) |
|
|
339
|
+
| `isAborted()` | Check if pipeline was aborted |
|
|
340
|
+
| `pause()` | Pause after the current stage completes |
|
|
341
|
+
| `resume()` | Resume a paused pipeline |
|
|
342
|
+
| `isPaused()` | Check if pipeline is paused |
|
|
343
|
+
| `exportState()` | Serialize stageResults and logs to a plain object |
|
|
344
|
+
| `importState(state)` | Restore stageResults and logs from a snapshot |
|
|
345
|
+
| `getStageResults()` | Synchronous snapshot of all stage results |
|
|
346
|
+
| `destroy()` | Run cleanup callbacks from all installed plugins |
|
|
347
|
+
| `subscribeProgress(listener)` | Subscribe to progress updates |
|
|
348
|
+
| `subscribeStageResults(listener)` | Subscribe to stageResults changes |
|
|
349
|
+
| `subscribeStepProgress(stepKey, listener)` | Subscribe to a specific stage's progress |
|
|
350
|
+
| `on(eventName, handler)` | Subscribe to any event (`step:<key>:start\|success\|error\|skipped\|progress`, `log`) |
|
|
351
|
+
| `onStepStart/Finish/Error(handler)` | Subscribe to stage lifecycle events |
|
|
352
|
+
| `getProgress()` | Get current progress snapshot |
|
|
353
|
+
| `getLogs()` | Get all pipeline logs |
|
|
354
|
+
| `clearStageResults()` | Reset results and progress |
|
|
355
|
+
|
|
356
|
+
### Stage parameters (PipelineStageConfig)
|
|
357
|
+
|
|
358
|
+
| Parameter | Description |
|
|
359
|
+
|---|---|
|
|
360
|
+
| `key` | Unique stage identifier |
|
|
361
|
+
| `request({ prev, allResults, sharedData })` | Main stage function — return value becomes the stage result |
|
|
362
|
+
| `condition({ prev, allResults, sharedData })` | If returns `false`, stage is skipped with status `"skipped"` |
|
|
363
|
+
| `before({ prev, allResults, sharedData })` | Pre-processing hook — returned value replaces `prev` passed to `request` |
|
|
364
|
+
| `after({ result, allResults, sharedData })` | Post-processing hook — returned value replaces the stage result |
|
|
365
|
+
| `errorHandler({ error, key, sharedData })` | Per-stage error handler |
|
|
366
|
+
| `retryCount` | Override retry count for this stage |
|
|
367
|
+
| `timeoutMs` | Override timeout for this stage |
|
|
368
|
+
| `pauseBefore` | Delay in ms before executing `request` |
|
|
369
|
+
| `pauseAfter` | Delay in ms after executing `request` |
|
|
370
|
+
|
|
371
|
+
### Stage execution flow
|
|
330
372
|
|
|
331
373
|
```
|
|
332
374
|
condition? → false → [status: skipped] → next stage
|
|
@@ -351,9 +393,60 @@ On error at any point:
|
|
|
351
393
|
└─► stage.errorHandler (if set) → middleware.onError → [status: error] → stop
|
|
352
394
|
```
|
|
353
395
|
|
|
396
|
+
### Full example
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
import { PipelineOrchestrator } from "rest-pipeline-js";
|
|
400
|
+
|
|
401
|
+
const orchestrator = new PipelineOrchestrator({
|
|
402
|
+
config: {
|
|
403
|
+
stages: [
|
|
404
|
+
{
|
|
405
|
+
key: "fetchUser",
|
|
406
|
+
request: async ({ sharedData }) => {
|
|
407
|
+
const res = await fetch(`/api/users/${sharedData.userId}`);
|
|
408
|
+
return res.json();
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
key: "processData",
|
|
413
|
+
condition: ({ prev }) => prev !== null,
|
|
414
|
+
before: ({ prev }) => ({ ...prev, processed: true }),
|
|
415
|
+
request: async ({ prev }) => prev,
|
|
416
|
+
after: ({ result }) => ({ ...result, finishedAt: Date.now() }),
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
middleware: {
|
|
420
|
+
beforeEach: ({ stage }) => console.log("Starting:", stage.key),
|
|
421
|
+
afterEach: ({ stage, result }) => console.log("Done:", stage.key, result.data),
|
|
422
|
+
onError: ({ stage, error }) => console.error("Error in", stage.key, error),
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
httpConfig: {
|
|
426
|
+
baseURL: "https://api.example.com",
|
|
427
|
+
retry: { attempts: 2, delayMs: 1000, backoffMultiplier: 2 },
|
|
428
|
+
cache: { enabled: true, ttlMs: 60000 },
|
|
429
|
+
},
|
|
430
|
+
sharedData: { userId: 42 },
|
|
431
|
+
options: { autoReset: true },
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
orchestrator.subscribeProgress((progress) => {
|
|
435
|
+
console.log("Stage:", progress.currentStage, "Statuses:", progress.stageStatuses);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
orchestrator.on("step:fetchUser:success", (payload) => {
|
|
439
|
+
console.log("fetchUser done:", payload.data);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const result = await orchestrator.run();
|
|
443
|
+
console.log("Pipeline finished:", result.success);
|
|
444
|
+
console.log("Stage results:", result.stageResults);
|
|
445
|
+
```
|
|
446
|
+
|
|
354
447
|
---
|
|
355
448
|
|
|
356
|
-
|
|
449
|
+
## Parallel stages
|
|
357
450
|
|
|
358
451
|
Group stages for concurrent execution using `parallel`:
|
|
359
452
|
|
|
@@ -368,7 +461,7 @@ const orchestrator = new PipelineOrchestrator({
|
|
|
368
461
|
{
|
|
369
462
|
key: "load-data",
|
|
370
463
|
parallel: [
|
|
371
|
-
{ key: "loadUsers",
|
|
464
|
+
{ key: "loadUsers", request: async () => fetchUsers() },
|
|
372
465
|
{ key: "loadProducts", request: async () => fetchProducts() },
|
|
373
466
|
{ key: "loadSettings", request: async () => fetchSettings() },
|
|
374
467
|
],
|
|
@@ -388,16 +481,14 @@ const orchestrator = new PipelineOrchestrator({
|
|
|
388
481
|
|
|
389
482
|
---
|
|
390
483
|
|
|
391
|
-
|
|
484
|
+
## Global middleware
|
|
392
485
|
|
|
393
486
|
Apply hooks to every stage without modifying individual stage configs:
|
|
394
487
|
|
|
395
488
|
```js
|
|
396
489
|
const orchestrator = new PipelineOrchestrator({
|
|
397
490
|
config: {
|
|
398
|
-
stages: [
|
|
399
|
-
/* ... */
|
|
400
|
-
],
|
|
491
|
+
stages: [ /* ... */ ],
|
|
401
492
|
middleware: {
|
|
402
493
|
beforeEach: async ({ stage, index, sharedData }) => {
|
|
403
494
|
console.log(`[${index}] Starting: ${stage.key}`);
|
|
@@ -419,7 +510,7 @@ Middleware runs in addition to (not instead of) per-stage `errorHandler`.
|
|
|
419
510
|
|
|
420
511
|
---
|
|
421
512
|
|
|
422
|
-
|
|
513
|
+
## Pause / Resume
|
|
423
514
|
|
|
424
515
|
Pause the pipeline after a stage and resume later:
|
|
425
516
|
|
|
@@ -444,7 +535,7 @@ await runPromise;
|
|
|
444
535
|
|
|
445
536
|
---
|
|
446
537
|
|
|
447
|
-
|
|
538
|
+
## Export / Import state
|
|
448
539
|
|
|
449
540
|
Save and restore the pipeline state across page reloads or sessions:
|
|
450
541
|
|
|
@@ -462,23 +553,21 @@ const orchestrator2 = new PipelineOrchestrator({ config });
|
|
|
462
553
|
orchestrator2.importState(saved);
|
|
463
554
|
|
|
464
555
|
console.log(orchestrator2.getProgress()); // restored progress
|
|
465
|
-
console.log(orchestrator2.getLogs());
|
|
556
|
+
console.log(orchestrator2.getLogs()); // restored logs (timestamps as Date objects)
|
|
466
557
|
```
|
|
467
558
|
|
|
468
559
|
`exportState()` returns `{ stageResults, logs }` — a plain JSON-serializable object. Timestamps in logs are stored as ISO strings and restored as `Date` objects on `importState`.
|
|
469
560
|
|
|
470
561
|
---
|
|
471
562
|
|
|
472
|
-
|
|
563
|
+
## Pipeline metrics
|
|
473
564
|
|
|
474
565
|
Observe pipeline execution without modifying stage logic:
|
|
475
566
|
|
|
476
567
|
```js
|
|
477
568
|
const orchestrator = new PipelineOrchestrator({
|
|
478
569
|
config: {
|
|
479
|
-
stages: [
|
|
480
|
-
/* ... */
|
|
481
|
-
],
|
|
570
|
+
stages: [ /* ... */ ],
|
|
482
571
|
metrics: {
|
|
483
572
|
onPipelineStart: ({ timestamp }) => {
|
|
484
573
|
console.log("Pipeline started at", new Date(timestamp).toISOString());
|
|
@@ -494,17 +583,17 @@ const orchestrator = new PipelineOrchestrator({
|
|
|
494
583
|
});
|
|
495
584
|
```
|
|
496
585
|
|
|
497
|
-
| Callback
|
|
498
|
-
|
|
499
|
-
| `onPipelineStart` | `{ timestamp }`
|
|
500
|
-
| `onPipelineEnd`
|
|
501
|
-
| `onStepDuration`
|
|
586
|
+
| Callback | Receives | Description |
|
|
587
|
+
|---|---|---|
|
|
588
|
+
| `onPipelineStart` | `{ timestamp }` | Fires at the beginning of `run()` |
|
|
589
|
+
| `onPipelineEnd` | `{ durationMs, success, stageResults }` | Fires when `run()` completes |
|
|
590
|
+
| `onStepDuration` | `{ stepKey, durationMs, status }` | Fires after every executed step |
|
|
502
591
|
|
|
503
592
|
---
|
|
504
593
|
|
|
505
|
-
|
|
594
|
+
## createPipeline() + pipe() builder
|
|
506
595
|
|
|
507
|
-
|
|
596
|
+
### createPipeline() — short factory
|
|
508
597
|
|
|
509
598
|
```js
|
|
510
599
|
import { createPipeline } from "rest-pipeline-js";
|
|
@@ -512,54 +601,51 @@ import { createPipeline } from "rest-pipeline-js";
|
|
|
512
601
|
const orchestrator = createPipeline(
|
|
513
602
|
[
|
|
514
603
|
{ key: "fetchUser", request: async () => fetchUser() },
|
|
515
|
-
{ key: "process",
|
|
604
|
+
{ key: "process", request: async ({ prev }) => process(prev) },
|
|
516
605
|
],
|
|
517
606
|
{
|
|
518
|
-
httpConfig:
|
|
519
|
-
sharedData:
|
|
607
|
+
httpConfig: { baseURL: "https://api.example.com" },
|
|
608
|
+
sharedData: { userId: 42 },
|
|
520
609
|
pipelineOptions: { continueOnError: false },
|
|
521
610
|
metrics: {
|
|
522
|
-
onStepDuration: ({ stepKey, durationMs }) =>
|
|
523
|
-
console.log(stepKey, durationMs),
|
|
611
|
+
onStepDuration: ({ stepKey, durationMs }) => console.log(stepKey, durationMs),
|
|
524
612
|
},
|
|
525
613
|
},
|
|
526
614
|
);
|
|
527
615
|
```
|
|
528
616
|
|
|
529
|
-
|
|
617
|
+
### pipe() — fluent builder
|
|
530
618
|
|
|
531
619
|
```js
|
|
532
620
|
import { pipe } from "rest-pipeline-js";
|
|
533
621
|
|
|
534
622
|
const orchestrator = pipe()
|
|
535
|
-
.step({ key: "auth",
|
|
623
|
+
.step({ key: "auth", request: async () => getToken() })
|
|
536
624
|
.step({ key: "fetchUser", request: async ({ prev }) => fetchUser(prev) })
|
|
537
625
|
.parallel([
|
|
538
|
-
{ key: "loadPosts",
|
|
626
|
+
{ key: "loadPosts", request: async () => fetchPosts() },
|
|
539
627
|
{ key: "loadNotifs", request: async () => fetchNotifications() },
|
|
540
628
|
])
|
|
541
629
|
.stream({
|
|
542
630
|
key: "liveUpdates",
|
|
543
|
-
stream: async function* () {
|
|
544
|
-
yield* subscribe("/events");
|
|
545
|
-
},
|
|
631
|
+
stream: async function* () { yield* subscribe("/events"); },
|
|
546
632
|
onChunk: (chunk) => updateUI(chunk),
|
|
547
633
|
})
|
|
548
634
|
.build({ httpConfig: { baseURL: "https://api.example.com" } });
|
|
549
635
|
```
|
|
550
636
|
|
|
551
|
-
| Builder method
|
|
552
|
-
|
|
553
|
-
| `.step(stage)`
|
|
554
|
-
| `.parallel(stages, options?)` | Add a parallel group (`key` auto-generated if omitted)
|
|
555
|
-
| `.subPipeline(item)`
|
|
556
|
-
| `.stream(stage)`
|
|
557
|
-
| `.build(options?)`
|
|
558
|
-
| `.toConfig(options?)`
|
|
637
|
+
| Builder method | Description |
|
|
638
|
+
|---|---|
|
|
639
|
+
| `.step(stage)` | Add a sequential stage |
|
|
640
|
+
| `.parallel(stages, options?)` | Add a parallel group (`key` auto-generated if omitted) |
|
|
641
|
+
| `.subPipeline(item)` | Embed a sub-pipeline as a stage |
|
|
642
|
+
| `.stream(stage)` | Add a stream stage (AsyncIterable) |
|
|
643
|
+
| `.build(options?)` | Create and return a `PipelineOrchestrator` |
|
|
644
|
+
| `.toConfig(options?)` | Return `PipelineConfig` without creating an orchestrator |
|
|
559
645
|
|
|
560
646
|
---
|
|
561
647
|
|
|
562
|
-
|
|
648
|
+
## validatePipelineConfig()
|
|
563
649
|
|
|
564
650
|
Catch configuration errors before runtime:
|
|
565
651
|
|
|
@@ -569,8 +655,8 @@ import { validatePipelineConfig } from "rest-pipeline-js";
|
|
|
569
655
|
const { valid, errors } = validatePipelineConfig({
|
|
570
656
|
stages: [
|
|
571
657
|
{ key: "step1", request: async () => data },
|
|
572
|
-
{ key: "step1", request: async () => other },
|
|
573
|
-
{ key: "",
|
|
658
|
+
{ key: "step1", request: async () => other }, // duplicate!
|
|
659
|
+
{ key: "", request: async () => other }, // empty key!
|
|
574
660
|
],
|
|
575
661
|
});
|
|
576
662
|
|
|
@@ -582,7 +668,7 @@ Validates: duplicate keys, empty/invalid keys, empty `stages` array, invalid fie
|
|
|
582
668
|
|
|
583
669
|
---
|
|
584
670
|
|
|
585
|
-
|
|
671
|
+
## Plugin system
|
|
586
672
|
|
|
587
673
|
Package reusable orchestrator behavior into plugins:
|
|
588
674
|
|
|
@@ -592,21 +678,16 @@ const loggingPlugin = {
|
|
|
592
678
|
install(orchestrator) {
|
|
593
679
|
const off = orchestrator.on("log", (event) => {
|
|
594
680
|
if (event.type === "step:success") console.log("✓", event.stepKey);
|
|
595
|
-
if (event.type === "step:error")
|
|
596
|
-
console.error("✗", event.stepKey, event.error);
|
|
681
|
+
if (event.type === "step:error") console.error("✗", event.stepKey, event.error);
|
|
597
682
|
});
|
|
598
|
-
return () => off();
|
|
683
|
+
return () => off(); // cleanup on orchestrator.destroy()
|
|
599
684
|
},
|
|
600
685
|
};
|
|
601
686
|
|
|
602
687
|
const orchestrator = new PipelineOrchestrator({
|
|
603
688
|
config: {
|
|
604
|
-
stages: [
|
|
605
|
-
|
|
606
|
-
],
|
|
607
|
-
options: {
|
|
608
|
-
plugins: [loggingPlugin, analyticsPlugin],
|
|
609
|
-
},
|
|
689
|
+
stages: [ /* ... */ ],
|
|
690
|
+
options: { plugins: [loggingPlugin, analyticsPlugin] },
|
|
610
691
|
},
|
|
611
692
|
});
|
|
612
693
|
|
|
@@ -619,7 +700,7 @@ orchestrator.destroy();
|
|
|
619
700
|
|
|
620
701
|
---
|
|
621
702
|
|
|
622
|
-
|
|
703
|
+
## Persist adapter
|
|
623
704
|
|
|
624
705
|
Automatically save and restore pipeline state across page reloads:
|
|
625
706
|
|
|
@@ -634,9 +715,7 @@ const localStorageAdapter = {
|
|
|
634
715
|
|
|
635
716
|
const orchestrator = new PipelineOrchestrator({
|
|
636
717
|
config: {
|
|
637
|
-
stages: [
|
|
638
|
-
/* ... */
|
|
639
|
-
],
|
|
718
|
+
stages: [ /* ... */ ],
|
|
640
719
|
options: { persistAdapter: localStorageAdapter },
|
|
641
720
|
},
|
|
642
721
|
});
|
|
@@ -658,7 +737,7 @@ Both methods may be async (useful for IndexedDB or remote storage).
|
|
|
658
737
|
|
|
659
738
|
---
|
|
660
739
|
|
|
661
|
-
|
|
740
|
+
## Stream stages (SSE / AsyncIterable)
|
|
662
741
|
|
|
663
742
|
A stage whose `stream` function returns an `AsyncIterable<T>`. The orchestrator collects all emitted chunks into an array (the stage result). `onChunk` is called for each chunk in real time.
|
|
664
743
|
|
|
@@ -668,7 +747,6 @@ const orchestrator = createPipeline([
|
|
|
668
747
|
{
|
|
669
748
|
key: "liveData",
|
|
670
749
|
stream: async function* ({ prev }) {
|
|
671
|
-
// prev is the result of "auth"
|
|
672
750
|
const source = new EventSource(`/api/stream?token=${prev}`);
|
|
673
751
|
yield* eventSourceToAsyncIterable(source);
|
|
674
752
|
},
|
|
@@ -691,27 +769,23 @@ const orchestrator = createPipeline([
|
|
|
691
769
|
|
|
692
770
|
---
|
|
693
771
|
|
|
694
|
-
|
|
772
|
+
## HTTP Adapter (custom fetch / edge environments)
|
|
695
773
|
|
|
696
774
|
Replace the built-in axios client with any HTTP implementation:
|
|
697
775
|
|
|
698
776
|
```js
|
|
699
777
|
const fetchAdapter = {
|
|
700
778
|
async request(config) {
|
|
701
|
-
const url
|
|
702
|
-
const res
|
|
703
|
-
method:
|
|
704
|
-
body:
|
|
779
|
+
const url = `${config.baseURL ?? ""}${config.url ?? ""}`;
|
|
780
|
+
const res = await fetch(url, {
|
|
781
|
+
method: config.method ?? "GET",
|
|
782
|
+
body: config.data ? JSON.stringify(config.data) : undefined,
|
|
705
783
|
headers: { "Content-Type": "application/json", ...config.headers },
|
|
706
|
-
signal:
|
|
784
|
+
signal: config.signal,
|
|
707
785
|
});
|
|
708
786
|
const data = await res.json();
|
|
709
|
-
return {
|
|
710
|
-
|
|
711
|
-
status: res.status,
|
|
712
|
-
statusText: res.statusText,
|
|
713
|
-
headers: Object.fromEntries(res.headers.entries()),
|
|
714
|
-
};
|
|
787
|
+
return { data, status: res.status, statusText: res.statusText,
|
|
788
|
+
headers: Object.fromEntries(res.headers.entries()) };
|
|
715
789
|
},
|
|
716
790
|
};
|
|
717
791
|
|
|
@@ -720,7 +794,6 @@ const client = createRestClient({
|
|
|
720
794
|
adapter: fetchAdapter,
|
|
721
795
|
// Auth, interceptors, sanitizeHeaders, metrics still work on top of the adapter
|
|
722
796
|
auth: { getToken: async () => token },
|
|
723
|
-
interceptors: { request: [addCorrelationId] },
|
|
724
797
|
});
|
|
725
798
|
```
|
|
726
799
|
|
|
@@ -734,9 +807,7 @@ type HttpAdapter = {
|
|
|
734
807
|
|
|
735
808
|
---
|
|
736
809
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
#### Example: use in Vue component
|
|
810
|
+
## Vue integration
|
|
740
811
|
|
|
741
812
|
```vue
|
|
742
813
|
<script setup>
|
|
@@ -746,13 +817,7 @@ import {
|
|
|
746
817
|
usePipelineRunVue,
|
|
747
818
|
} from "rest-pipeline-js/vue";
|
|
748
819
|
|
|
749
|
-
const orchestrator = new PipelineOrchestrator({
|
|
750
|
-
config: {
|
|
751
|
-
stages: [
|
|
752
|
-
/* ... */
|
|
753
|
-
],
|
|
754
|
-
},
|
|
755
|
-
});
|
|
820
|
+
const orchestrator = new PipelineOrchestrator({ config: { stages: [ /* ... */ ] } });
|
|
756
821
|
const progress = usePipelineProgressVue(orchestrator);
|
|
757
822
|
const { run, running, result, error, abort, pause, resume, rerunStep } =
|
|
758
823
|
usePipelineRunVue(orchestrator);
|
|
@@ -771,23 +836,21 @@ const { run, running, result, error, abort, pause, resume, rerunStep } =
|
|
|
771
836
|
</template>
|
|
772
837
|
```
|
|
773
838
|
|
|
774
|
-
|
|
839
|
+
Composables (import from `rest-pipeline-js/vue`):
|
|
775
840
|
|
|
776
|
-
|
|
|
777
|
-
|
|
778
|
-
| `usePipelineProgressVue(orchestrator)`
|
|
779
|
-
| `usePipelineRunVue(orchestrator)`
|
|
780
|
-
| `usePipelineStepEventVue(orchestrator, stepKey, eventType)` | `Ref<any>`
|
|
781
|
-
| `usePipelineLogsVue(orchestrator)`
|
|
782
|
-
| `useRerunPipelineStepVue(orchestrator)`
|
|
783
|
-
| `useRestClientVue(config)`
|
|
784
|
-
| `usePipelineStageResultVue(orchestrator, stepKey)`
|
|
841
|
+
| Composable | Returns | Description |
|
|
842
|
+
|---|---|---|
|
|
843
|
+
| `usePipelineProgressVue(orchestrator)` | `Ref<PipelineProgress>` | Reactive progress |
|
|
844
|
+
| `usePipelineRunVue(orchestrator)` | `{ run, running, result, error, stageResults, abort, pause, resume, rerunStep, clearStageResults }` | Run pipeline and get reactive state |
|
|
845
|
+
| `usePipelineStepEventVue(orchestrator, stepKey, eventType)` | `Ref<any>` | Last payload for a specific step event |
|
|
846
|
+
| `usePipelineLogsVue(orchestrator)` | `Ref<log[]>` | Reactive logs |
|
|
847
|
+
| `useRerunPipelineStepVue(orchestrator)` | `function` | Bound `rerunStep` |
|
|
848
|
+
| `useRestClientVue(config)` | `ComputedRef<RestClient>` | Reactive REST client |
|
|
849
|
+
| `usePipelineStageResultVue(orchestrator, stepKey)` | `Ref<PipelineStepResult \| null>` | Reactive result of a single stage |
|
|
785
850
|
|
|
786
851
|
---
|
|
787
852
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
#### Example: use in React component
|
|
853
|
+
## React integration
|
|
791
854
|
|
|
792
855
|
```jsx
|
|
793
856
|
import { useRef } from "react";
|
|
@@ -797,13 +860,7 @@ import {
|
|
|
797
860
|
usePipelineRunReact,
|
|
798
861
|
} from "rest-pipeline-js/react";
|
|
799
862
|
|
|
800
|
-
const orchestrator = new PipelineOrchestrator({
|
|
801
|
-
config: {
|
|
802
|
-
stages: [
|
|
803
|
-
/* ... */
|
|
804
|
-
],
|
|
805
|
-
},
|
|
806
|
-
});
|
|
863
|
+
const orchestrator = new PipelineOrchestrator({ config: { stages: [ /* ... */ ] } });
|
|
807
864
|
|
|
808
865
|
export function PipelineComponent() {
|
|
809
866
|
const progress = usePipelineProgressReact(orchestrator);
|
|
@@ -813,16 +870,12 @@ export function PipelineComponent() {
|
|
|
813
870
|
return (
|
|
814
871
|
<div>
|
|
815
872
|
<div>Current stage: {progress.currentStage}</div>
|
|
816
|
-
<button onClick={() => run()} disabled={running}>
|
|
817
|
-
|
|
818
|
-
</button>
|
|
819
|
-
<button onClick={() => abort()} disabled={!running}>
|
|
820
|
-
Abort
|
|
821
|
-
</button>
|
|
873
|
+
<button onClick={() => run()} disabled={running}>Start</button>
|
|
874
|
+
<button onClick={() => abort()} disabled={!running}>Abort</button>
|
|
822
875
|
<button onClick={() => pause()}>Pause</button>
|
|
823
876
|
<button onClick={() => resume()}>Resume</button>
|
|
824
877
|
{result && <div>Done: {JSON.stringify(result)}</div>}
|
|
825
|
-
{error
|
|
878
|
+
{error && <div>Error: {error.message}</div>}
|
|
826
879
|
</div>
|
|
827
880
|
);
|
|
828
881
|
}
|
|
@@ -830,25 +883,25 @@ export function PipelineComponent() {
|
|
|
830
883
|
|
|
831
884
|
Hooks (import from `rest-pipeline-js/react`):
|
|
832
885
|
|
|
833
|
-
| Hook
|
|
834
|
-
|
|
835
|
-
| `usePipelineProgressReact(orchestrator)`
|
|
836
|
-
| `usePipelineRunReact(orchestrator)`
|
|
837
|
-
| `usePipelineStepEventReact(orchestrator, stepKey, eventType)` | `any`
|
|
838
|
-
| `usePipelineLogsReact(orchestrator)`
|
|
839
|
-
| `useRerunPipelineStepReact(orchestrator)`
|
|
840
|
-
| `useRestClientReact(config)`
|
|
841
|
-
| `usePipelineStageResultReact(orchestrator, stepKey)`
|
|
886
|
+
| Hook | Returns | Description |
|
|
887
|
+
|---|---|---|
|
|
888
|
+
| `usePipelineProgressReact(orchestrator)` | `PipelineProgress` | Reactive progress |
|
|
889
|
+
| `usePipelineRunReact(orchestrator)` | `[run, { running, result, error, stageResults, abort, pause, resume, rerunStep }]` | Run pipeline and get state |
|
|
890
|
+
| `usePipelineStepEventReact(orchestrator, stepKey, eventType)` | `any` | Last payload for a specific step event |
|
|
891
|
+
| `usePipelineLogsReact(orchestrator)` | `log[]` | Reactive logs |
|
|
892
|
+
| `useRerunPipelineStepReact(orchestrator)` | `function` | Bound `rerunStep` |
|
|
893
|
+
| `useRestClientReact(config)` | `RestClient` | Memoized REST client |
|
|
894
|
+
| `usePipelineStageResultReact(orchestrator, stepKey)` | `PipelineStepResult \| null` | Result of a single stage |
|
|
842
895
|
|
|
843
896
|
---
|
|
844
897
|
|
|
845
898
|
## Entry points
|
|
846
899
|
|
|
847
|
-
| Entry point
|
|
848
|
-
|
|
849
|
-
| `rest-pipeline-js`
|
|
850
|
-
| `rest-pipeline-js/vue`
|
|
851
|
-
| `rest-pipeline-js/react` | React projects | Core + React hooks
|
|
900
|
+
| Entry point | Use for | Contents |
|
|
901
|
+
|---|---|---|
|
|
902
|
+
| `rest-pipeline-js` | Core only | `PipelineOrchestrator`, `createRestClient`, types, utilities. No Vue/React. |
|
|
903
|
+
| `rest-pipeline-js/vue` | Vue projects | Core + Vue composables |
|
|
904
|
+
| `rest-pipeline-js/react` | React projects | Core + React hooks |
|
|
852
905
|
|
|
853
906
|
```js
|
|
854
907
|
// Core only
|
|
@@ -858,46 +911,67 @@ import { createRestClient, PipelineOrchestrator } from "rest-pipeline-js";
|
|
|
858
911
|
import { PipelineOrchestrator, usePipelineRunVue } from "rest-pipeline-js/vue";
|
|
859
912
|
|
|
860
913
|
// React
|
|
861
|
-
import {
|
|
862
|
-
PipelineOrchestrator,
|
|
863
|
-
usePipelineRunReact,
|
|
864
|
-
} from "rest-pipeline-js/react";
|
|
914
|
+
import { PipelineOrchestrator, usePipelineRunReact } from "rest-pipeline-js/react";
|
|
865
915
|
```
|
|
866
916
|
|
|
867
|
-
`sideEffects: false` — unused entry points are tree-shaken. `react
|
|
917
|
+
`sideEffects: false` — unused entry points are tree-shaken. `react` / `react-dom` are `peerDependencies`.
|
|
868
918
|
|
|
869
919
|
---
|
|
870
920
|
|
|
871
|
-
##
|
|
872
|
-
|
|
873
|
-
- Node.js >= 14.0.0
|
|
874
|
-
- Modern browser with ES2019+ support
|
|
875
|
-
|
|
876
|
-
---
|
|
877
|
-
|
|
878
|
-
## Vue Demo
|
|
879
|
-
|
|
880
|
-
A live interactive demo of the pipeline running against a real flight-search API — 4 sequential stages: airport lookup, availability, ancillary services, and seat map.
|
|
921
|
+
## Architecture
|
|
881
922
|
|
|
882
|
-
```bash
|
|
883
|
-
git clone https://github.com/macrulezru/pipeline-js.git
|
|
884
|
-
cd pipeline-js
|
|
885
|
-
npm install
|
|
886
|
-
npm run demo:vue
|
|
887
923
|
```
|
|
888
|
-
|
|
889
|
-
|
|
924
|
+
rest-pipeline-js
|
|
925
|
+
│
|
|
926
|
+
├── createRestClient (HttpConfig) → RestClient
|
|
927
|
+
│ ├── RequestExecutor — retry + backoff + Retry-After + AbortController timeout
|
|
928
|
+
│ ├── CacheManager — in-memory TTL cache for GET responses
|
|
929
|
+
│ ├── RateLimiter — concurrency + req/interval sliding window
|
|
930
|
+
│ ├── AuthProvider — Bearer injection; 401 refresh + one retry
|
|
931
|
+
│ ├── MetricsCollector — onRequestStart / onRequestEnd callbacks
|
|
932
|
+
│ ├── HeaderSanitizer — masks sensitive headers before metrics callbacks
|
|
933
|
+
│ └── HttpAdapter — pluggable transport (default: axios; swap for fetch / edge)
|
|
934
|
+
│
|
|
935
|
+
├── PipelineOrchestrator (config, httpConfig?, sharedData?, options?)
|
|
936
|
+
│ ├── StageRunner — sequential execution loop; parallel via Promise.all
|
|
937
|
+
│ │ condition → pauseBefore → before → request → after → pauseAfter
|
|
938
|
+
│ ├── MiddlewareRunner — beforeEach / afterEach / onError across all stages
|
|
939
|
+
│ ├── EventBus — on() / emit(); step:start|success|error|skipped|progress, log
|
|
940
|
+
│ ├── ProgressTracker — subscribeProgress / subscribeStageResults / getProgress
|
|
941
|
+
│ ├── AbortController — abort() cancels current HTTP request via AbortSignal
|
|
942
|
+
│ ├── PauseController — pause() / resume() inter-stage checkpoints
|
|
943
|
+
│ ├── MetricsHooks — onPipelineStart / onPipelineEnd / onStepDuration
|
|
944
|
+
│ ├── StateSerializer — exportState() / importState() (stageResults + logs)
|
|
945
|
+
│ ├── PersistAdapter — pluggable save/load; auto-save after each stage
|
|
946
|
+
│ └── PluginManager — install() + destroy() lifecycle
|
|
947
|
+
│
|
|
948
|
+
├── PipelineBuilder (pipe())
|
|
949
|
+
│ .step() / .parallel() / .subPipeline() / .stream() → .build() / .toConfig()
|
|
950
|
+
│
|
|
951
|
+
├── createPipeline() — short factory wrapping new PipelineOrchestrator()
|
|
952
|
+
│
|
|
953
|
+
├── validatePipelineConfig() — duplicate keys, empty keys, type checks, recursive
|
|
954
|
+
│
|
|
955
|
+
├── /vue (separate entry point)
|
|
956
|
+
│ usePipelineRunVue / usePipelineProgressVue / usePipelineLogsVue
|
|
957
|
+
│ usePipelineStepEventVue / useRestClientVue / usePipelineStageResultVue
|
|
958
|
+
│
|
|
959
|
+
└── /react (separate entry point)
|
|
960
|
+
usePipelineRunReact / usePipelineProgressReact / usePipelineLogsReact
|
|
961
|
+
usePipelineStepEventReact / useRestClientReact / usePipelineStageResultReact
|
|
962
|
+
```
|
|
890
963
|
|
|
891
964
|
---
|
|
892
965
|
|
|
893
|
-
##
|
|
966
|
+
## Bundle size & peer dependencies
|
|
894
967
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
968
|
+
| Entry point | Peer deps | Notes |
|
|
969
|
+
|---|---|---|
|
|
970
|
+
| `rest-pipeline-js` | — | Core — orchestrator, HTTP client, utilities. Depends on `axios`. |
|
|
971
|
+
| `rest-pipeline-js/vue` | `vue ^3.3` | Core + Vue composables |
|
|
972
|
+
| `rest-pipeline-js/react` | `react ^19`, `react-dom ^19` | Core + React hooks |
|
|
973
|
+
|
|
974
|
+
The package ships as tree-shakeable ESM (`dist/esm/`) and CommonJS (`dist/cjs/`). The `/vue` and `/react` entry points are code-split — importing one does not bundle the other.
|
|
901
975
|
|
|
902
976
|
---
|
|
903
977
|
|
|
@@ -911,6 +985,18 @@ MIT
|
|
|
911
985
|
|
|
912
986
|
Danil Lisin Vladimirovich aka Macrulez
|
|
913
987
|
|
|
914
|
-
GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru](https://macrulez.ru/)
|
|
988
|
+
GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru/en](https://macrulez.ru/en)
|
|
989
|
+
|
|
990
|
+
Bugs and questions — [issues](https://github.com/macrulezru/pipeline-js/issues)
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## 💖 Support the project
|
|
995
|
+
|
|
996
|
+
Open source takes time and effort. If my work saves you time or brings value, consider supporting further development.
|
|
997
|
+
|
|
998
|
+
<a href="https://donate.cryptocloud.plus/M6O34NIN" target="_blank">
|
|
999
|
+
<img src="https://img.shields.io/badge/Donate-CryptoCloud-8A2BE2?style=for-the-badge&logo=cryptocurrency&logoColor=white" alt="Donate via CryptoCloud">
|
|
1000
|
+
</a>
|
|
915
1001
|
|
|
916
|
-
|
|
1002
|
+
Thank you for being part of this journey. ❤️
|