rest-pipeline-js 1.3.14 → 1.4.1
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 +21 -0
- package/README.md +169 -35
- package/dist/cjs/cache.js +15 -0
- package/dist/cjs/circuit-breaker.js +84 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/pipeline-builder.js +22 -3
- package/dist/cjs/pipeline-orchestrator.js +159 -97
- package/dist/cjs/rest-client.js +97 -20
- package/dist/cjs/types.js +13 -0
- package/dist/esm/cache.d.ts +4 -0
- package/dist/esm/cache.js +15 -0
- package/dist/esm/circuit-breaker.d.ts +33 -0
- package/dist/esm/circuit-breaker.js +79 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/pipeline-builder.d.ts +24 -9
- package/dist/esm/pipeline-builder.js +22 -3
- package/dist/esm/pipeline-orchestrator.d.ts +20 -0
- package/dist/esm/pipeline-orchestrator.js +159 -97
- package/dist/esm/rest-client.d.ts +10 -0
- package/dist/esm/rest-client.js +97 -20
- package/dist/esm/types.d.ts +77 -2
- package/dist/esm/types.js +11 -0
- package/dist/esm/usePipelineStepEvents-react.d.ts +1 -0
- package/dist/esm/usePipelineStepEvents-vue.d.ts +3 -0
- package/dist/esm/useRestClient-react.d.ts +5 -0
- package/dist/esm/useRestClient-vue.d.ts +5 -0
- package/package.json +103 -74
- package/CHANGELOG.md +0 -100
- package/demo/App.vue +0 -122
- package/demo/index.html +0 -22
- package/demo/main.js +0 -5
- package/demo/style.css +0 -857
- package/demo/views/CacheDemo.vue +0 -599
- package/demo/views/FlightDemo.vue +0 -481
- package/demo/views/ParallelDemo.vue +0 -546
- package/demo/views/RetryDemo.vue +0 -506
- package/eslint.config.js +0 -40
- package/react-shim.d.ts +0 -1
- package/src/cache.ts +0 -76
- package/src/error-handler.ts +0 -10
- package/src/index.ts +0 -10
- package/src/pipeline-builder.ts +0 -158
- package/src/pipeline-orchestrator.ts +0 -1299
- package/src/pipeline-validator.ts +0 -151
- package/src/progress-tracker.ts +0 -60
- package/src/rate-limiter.ts +0 -76
- package/src/react.ts +0 -12
- package/src/request-executor.ts +0 -168
- package/src/rest-client.ts +0 -486
- package/src/tsconfig.json +0 -10
- package/src/types.ts +0 -633
- package/src/usePipelineProgress-react.ts +0 -21
- package/src/usePipelineProgress-vue.ts +0 -17
- package/src/usePipelineRun-react.ts +0 -52
- package/src/usePipelineRun-vue.ts +0 -63
- package/src/usePipelineStageResult-react.ts +0 -32
- package/src/usePipelineStageResult-vue.ts +0 -34
- package/src/usePipelineStepEvents-react.ts +0 -49
- package/src/usePipelineStepEvents-vue.ts +0 -48
- package/src/useRestClient-react.ts +0 -12
- package/src/useRestClient-vue.ts +0 -12
- package/src/vue-demo/demo.css +0 -768
- package/src/vue-demo/demo.vue +0 -621
- package/src/vue-demo/index.html +0 -21
- package/src/vue-demo/main.js +0 -4
- package/src/vue.ts +0 -12
- package/tests/error-handler.test.ts +0 -10
- package/tests/pipeline-orchestrator.test.ts +0 -1182
- package/tests/progress-tracker.test.ts +0 -13
- package/tests/react-hooks.test.ts +0 -61
- package/tests/request-executor.test.ts +0 -39
- package/tests/rest-client.test.ts +0 -259
- package/tests/types.test.ts +0 -105
- package/tests/vue-hooks.test.ts +0 -57
- package/tsconfig.cjs.json +0 -17
- package/tsconfig.esm.json +0 -16
- package/tsconfig.json +0 -17
- package/vite.config.js +0 -25
- package/vitest.config.ts +0 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Danil Lisin Vladimirovich (macrulez)
|
|
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
CHANGED
|
@@ -23,12 +23,15 @@ Flexible, modular pipeline orchestrator for REST APIs — sequential and paralle
|
|
|
23
23
|
- [Auth Provider](#auth-provider)
|
|
24
24
|
- [Log Sanitization](#log-sanitization)
|
|
25
25
|
- [RequestExecutor](#requestexecutor)
|
|
26
|
+
- [Circuit breaker](#circuit-breaker)
|
|
26
27
|
- [PipelineOrchestrator](#pipelineorchestrator)
|
|
28
|
+
- [Error recovery (errorHandler + recoverStep)](#error-recovery-errorhandler--recoverstep)
|
|
27
29
|
- [Parallel stages](#parallel-stages)
|
|
28
30
|
- [Global middleware](#global-middleware)
|
|
29
31
|
- [Pause / Resume](#pause--resume)
|
|
30
32
|
- [Export / Import state](#export--import-state)
|
|
31
33
|
- [Pipeline metrics](#pipeline-metrics)
|
|
34
|
+
- [Correlating a run (runId)](#correlating-a-run-runid)
|
|
32
35
|
- [createPipeline() + pipe() builder](#createpipeline--pipe-builder)
|
|
33
36
|
- [validatePipelineConfig()](#validatepipelineconfig)
|
|
34
37
|
- [Plugin system](#plugin-system)
|
|
@@ -45,15 +48,16 @@ Flexible, modular pipeline orchestrator for REST APIs — sequential and paralle
|
|
|
45
48
|
|
|
46
49
|
## Features
|
|
47
50
|
|
|
48
|
-
- **`createRestClient()`** — full-featured HTTP client built on top of axios: retry with exponential backoff and `Retry-After` support, response caching
|
|
49
|
-
- **`PipelineOrchestrator`** — sequential and parallel stage execution; each stage has `condition`, `before`, `request`, `after`, `errorHandler` hooks; `sharedData` pool shared across all stages
|
|
51
|
+
- **`createRestClient()`** — full-featured HTTP client built on top of axios: retry with exponential backoff and `Retry-After` support, response caching (incl. targeted `invalidateCache()`), rate limiting (concurrency + req/interval), circuit breaker, auth provider with automatic 401 refresh and optional token caching, request cancellation by key, custom HTTP adapters
|
|
52
|
+
- **`PipelineOrchestrator`** — sequential and parallel stage execution; each stage has `condition`, `before`, `request`, `after`, `errorHandler` hooks (all receive the pipeline's `AbortSignal`); `sharedData` pool shared across all stages
|
|
53
|
+
- **Error recovery** — `errorHandler` can return `recoverStep(data)` to turn a failed stage back into a successful one and keep the pipeline going, instead of only transforming the error
|
|
50
54
|
- **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
|
|
55
|
+
- **Parallel groups** — multiple stages run concurrently via `Promise.all`, or through a bounded pool via `concurrency`; single failure stops the group
|
|
56
|
+
- **Pause / Resume / Abort** — `pause()` waits after the current stage; `resume()` continues; `abort()` cancels the current HTTP request and propagates its `AbortSignal` into every stage hook so custom `request`/`before`/`after` functions can cancel their own work too
|
|
53
57
|
- **Export / Import state** — serialize `stageResults` + logs to a plain object; restore on the next page load
|
|
54
58
|
- **Stream stages** — `stream: async function*` for SSE / any `AsyncIterable`; `onChunk` callback in real time; abort-aware
|
|
55
|
-
- **Pipeline metrics** — `onPipelineStart`, `onPipelineEnd`, `onStepDuration` callbacks
|
|
56
|
-
- **`createPipeline()` / `pipe()` builder** — short factory and fluent builder API for common patterns
|
|
59
|
+
- **Pipeline metrics & run correlation** — `onPipelineStart`, `onPipelineEnd`, `onStepDuration` callbacks, plus a `runId` (also on `getRunId()`, log entries, and step events) shared by every callback/event from the same run
|
|
60
|
+
- **`createPipeline()` / `pipe()` builder** — short factory and fluent builder API for common patterns; in TypeScript, `pipe().step()` chains infer `prev`'s type from the previous step automatically
|
|
57
61
|
- **`validatePipelineConfig()`** — catch duplicate keys, empty keys, type errors before runtime
|
|
58
62
|
- **Plugin system** — install reusable behavior (logging, analytics, etc.); cleanup via `destroy()`
|
|
59
63
|
- **Persist adapter** — pluggable save/load interface; auto-save after each stage
|
|
@@ -168,7 +172,9 @@ Creates a REST client with advanced HTTP features.
|
|
|
168
172
|
| `request(url, config?)` | Generic request |
|
|
169
173
|
| `cancellableRequest(key, url, config?)` | Request cancellable by key |
|
|
170
174
|
| `cancelRequest(key)` | Cancel request by key |
|
|
171
|
-
| `clearCache()` | Clear this client's response cache |
|
|
175
|
+
| `clearCache()` | Clear this client's entire response cache |
|
|
176
|
+
| `invalidateCache(matcher)` | Clear only cache entries whose URL matches `matcher` (substring, `RegExp`, or `(info) => boolean`); returns the number of entries removed |
|
|
177
|
+
| `getCircuitBreakerState()` | `"closed" \| "open" \| "half-open"`, or `null` if `circuitBreaker` isn't configured |
|
|
172
178
|
|
|
173
179
|
### HttpConfig options
|
|
174
180
|
|
|
@@ -190,11 +196,13 @@ Creates a REST client with advanced HTTP features.
|
|
|
190
196
|
| `rateLimit.intervalMs` | Time window size in ms |
|
|
191
197
|
| `metrics.onRequestStart` | Callback on request start |
|
|
192
198
|
| `metrics.onRequestEnd` | Callback on request end (includes duration and bytes) |
|
|
193
|
-
| `auth.getToken` | Async function returning a Bearer token (called before every request)
|
|
199
|
+
| `auth.getToken` | Async function returning a Bearer token (called before every request, unless `auth.tokenTtlMs` is set) |
|
|
194
200
|
| `auth.onUnauthorized` | Optional async callback on 401 — refresh the token here; request is retried once |
|
|
201
|
+
| `auth.tokenTtlMs` | Cache `getToken()`'s result for this many ms instead of calling it before every request; invalidated automatically on 401 |
|
|
195
202
|
| `sanitizeHeaders` | Mask sensitive headers in metrics callbacks (default: `false`) |
|
|
196
203
|
| `sensitiveHeaders` | Additional headers to mask (extends `DEFAULT_SENSITIVE_HEADERS`) |
|
|
197
204
|
| `adapter` | Custom HTTP adapter (e.g. native `fetch`) — replaces built-in axios |
|
|
205
|
+
| `circuitBreaker` | See [Circuit breaker](#circuit-breaker) — `{ failureThreshold, openMs, successThreshold?, isFailure? }` |
|
|
198
206
|
|
|
199
207
|
### Per-request cache override
|
|
200
208
|
|
|
@@ -206,6 +214,18 @@ const res = await client.get("/data", {
|
|
|
206
214
|
});
|
|
207
215
|
```
|
|
208
216
|
|
|
217
|
+
### Targeted cache invalidation
|
|
218
|
+
|
|
219
|
+
`clearCache()` wipes the entire response cache. To invalidate only the entries affected by a mutation (e.g. after a `POST`/`PUT`/`DELETE`), use `invalidateCache()` instead — it accepts a substring, a `RegExp`, or a predicate over `{ method, url }`, and returns how many entries were removed:
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
await client.post("/users/1/orders", newOrder);
|
|
223
|
+
|
|
224
|
+
client.invalidateCache("/users/1"); // substring match on the cached URL
|
|
225
|
+
client.invalidateCache(/^https:\/\/api\.example\.com\/users\/\d+$/);
|
|
226
|
+
client.invalidateCache(({ method, url }) => method === "GET" && url.includes("/orders"));
|
|
227
|
+
```
|
|
228
|
+
|
|
209
229
|
### Full example
|
|
210
230
|
|
|
211
231
|
```js
|
|
@@ -269,6 +289,21 @@ const client = createRestClient({
|
|
|
269
289
|
const res = await client.get("/profile");
|
|
270
290
|
```
|
|
271
291
|
|
|
292
|
+
### Caching the token
|
|
293
|
+
|
|
294
|
+
If `getToken()` is expensive (e.g. it talks to a secure storage or refresh endpoint), set `tokenTtlMs` to reuse the result across requests instead of calling `getToken()` before every single one. The cache is invalidated automatically on a `401`, so the next request always re-fetches a fresh token before retrying:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
const client = createRestClient({
|
|
298
|
+
baseURL: "https://api.example.com",
|
|
299
|
+
auth: {
|
|
300
|
+
getToken: async () => requestTokenFromSecureEnclave(), // expensive
|
|
301
|
+
onUnauthorized: async () => refreshAccessToken(),
|
|
302
|
+
tokenTtlMs: 5 * 60_000, // reuse for up to 5 minutes
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
272
307
|
---
|
|
273
308
|
|
|
274
309
|
## Log Sanitization
|
|
@@ -334,6 +369,41 @@ When the server returns a `Retry-After` header (numeric seconds or HTTP-date), t
|
|
|
334
369
|
|
|
335
370
|
---
|
|
336
371
|
|
|
372
|
+
## Circuit breaker
|
|
373
|
+
|
|
374
|
+
Protect a failing backend (and your own app) from piling up retries/timeouts: after `failureThreshold` consecutive failures, the client stops calling the network entirely for `openMs` and rejects requests immediately with a `CircuitOpenError` (`code: "CIRCUIT_OPEN"`). After `openMs`, it lets a probe request through (`half-open`); success closes the circuit again, failure re-opens it.
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
import { createRestClient, CircuitOpenError } from "rest-pipeline-js";
|
|
378
|
+
|
|
379
|
+
const client = createRestClient({
|
|
380
|
+
baseURL: "https://api.example.com",
|
|
381
|
+
circuitBreaker: {
|
|
382
|
+
failureThreshold: 5, // open after 5 consecutive failures
|
|
383
|
+
openMs: 30_000, // stay open for 30s before probing again
|
|
384
|
+
successThreshold: 2, // need 2 successful probes to fully close
|
|
385
|
+
isFailure: (error) => error.status === undefined || error.status >= 500, // ignore 4xx
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await client.get("/flaky-endpoint");
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (err instanceof CircuitOpenError) {
|
|
393
|
+
// rejected locally — no network call was made
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
client.getCircuitBreakerState(); // "closed" | "open" | "half-open"
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- Works on top of retry, cache, rate limiting, auth, and custom `adapter`s — it sits around the actual network call, same as those features.
|
|
401
|
+
- Each retry attempt (from `RequestExecutor`/`request.retry`) counts as its own pass through the breaker, so a flaky endpoint with retries enabled opens the circuit faster, not slower.
|
|
402
|
+
- Cancelled/aborted requests are never counted as failures.
|
|
403
|
+
- Not set by default — without `circuitBreaker`, behavior is unchanged.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
337
407
|
## PipelineOrchestrator
|
|
338
408
|
|
|
339
409
|
Main class for building and managing a pipeline of sequential (and parallel) stages.
|
|
@@ -363,6 +433,7 @@ new PipelineOrchestrator({
|
|
|
363
433
|
| `exportState()` | Serialize stageResults and logs to a plain object |
|
|
364
434
|
| `importState(state)` | Restore stageResults and logs from a snapshot |
|
|
365
435
|
| `getStageResults()` | Synchronous snapshot of all stage results |
|
|
436
|
+
| `getRunId()` | ID of the current/last `run()` or `rerunStep()` — see [Correlating a run](#correlating-a-run-runid) |
|
|
366
437
|
| `destroy()` | Run cleanup callbacks from all installed plugins |
|
|
367
438
|
| `subscribeProgress(listener)` | Subscribe to progress updates |
|
|
368
439
|
| `subscribeStageResults(listener)` | Subscribe to stageResults changes |
|
|
@@ -375,18 +446,37 @@ new PipelineOrchestrator({
|
|
|
375
446
|
|
|
376
447
|
### Stage parameters (PipelineStageConfig)
|
|
377
448
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
|
381
|
-
|
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
449
|
+
Every hook below also receives `signal: AbortSignal` in its params object — the same signal used by `orchestrator.abort()`. Pass it down to `fetch`/`axios`/etc. inside `request`/`before`/`after` so cancellation actually stops in-flight work, not just the pipeline's bookkeeping.
|
|
450
|
+
|
|
451
|
+
| Parameter | Description |
|
|
452
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
453
|
+
| `key` | Unique stage identifier |
|
|
454
|
+
| `request({ prev, allResults, sharedData, signal })` | Main stage function — return value becomes the stage result |
|
|
455
|
+
| `condition({ prev, allResults, sharedData, signal })` | If returns `false`, stage is skipped with status `"skipped"` |
|
|
456
|
+
| `before({ prev, allResults, sharedData, signal })` | Pre-processing hook — returned value replaces `prev` passed to `request` |
|
|
457
|
+
| `after({ result, allResults, sharedData, signal })` | Post-processing hook — returned value replaces the stage result |
|
|
458
|
+
| `errorHandler({ error, key, sharedData, signal })` | Per-stage error handler — see [Error recovery](#error-recovery-errorhandler--recoverstep) below |
|
|
459
|
+
| `retryCount` | Override retry count for this stage |
|
|
460
|
+
| `timeoutMs` | Override timeout for this stage |
|
|
461
|
+
| `pauseBefore` | Delay in ms before executing `request` |
|
|
462
|
+
| `pauseAfter` | Delay in ms after executing `request` |
|
|
463
|
+
|
|
464
|
+
### Error recovery (`errorHandler` + `recoverStep`)
|
|
465
|
+
|
|
466
|
+
By default, whatever `errorHandler` returns is wrapped into an `ApiError` and the stage stays `"error"` — it can transform/enrich the error but not turn the failure into a success. Return `recoverStep(data)` to recover the stage instead: it's committed exactly like a successful stage (status `"success"`, `afterEach` middleware, metrics, persistence) and the pipeline continues normally.
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
import { recoverStep } from "rest-pipeline-js";
|
|
470
|
+
|
|
471
|
+
{
|
|
472
|
+
key: "fetchPrice",
|
|
473
|
+
request: async () => fetchPriceFromApi(),
|
|
474
|
+
errorHandler: ({ error }) => {
|
|
475
|
+
if (isNetworkError(error)) return recoverStep(0); // fall back to a default and continue
|
|
476
|
+
return error; // anything else: keep failing as before
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
```
|
|
390
480
|
|
|
391
481
|
### Stage execution flow
|
|
392
482
|
|
|
@@ -410,7 +500,9 @@ middleware.afterEach
|
|
|
410
500
|
[status: success] → next stage
|
|
411
501
|
|
|
412
502
|
On error at any point:
|
|
413
|
-
└─► stage.errorHandler (if set)
|
|
503
|
+
└─► stage.errorHandler (if set)
|
|
504
|
+
├─► returns recoverStep(data) → [status: success] → next stage
|
|
505
|
+
└─► otherwise → middleware.onError → [status: error] → stop
|
|
414
506
|
```
|
|
415
507
|
|
|
416
508
|
### Full example
|
|
@@ -501,11 +593,28 @@ const orchestrator = new PipelineOrchestrator({
|
|
|
501
593
|
});
|
|
502
594
|
```
|
|
503
595
|
|
|
504
|
-
- All stages in a `parallel` group run simultaneously via `Promise.all
|
|
596
|
+
- All stages in a `parallel` group run simultaneously via `Promise.all` — unless `concurrency` is set (see below).
|
|
505
597
|
- If **any** stage in the group fails, the pipeline stops and marks `success: false`.
|
|
506
598
|
- Each parallel stage has its own key and result in `stageResults`.
|
|
507
599
|
- `rerunStep(key)` works for stages inside parallel groups too.
|
|
508
600
|
|
|
601
|
+
### Limiting concurrency
|
|
602
|
+
|
|
603
|
+
For fan-out over many items (e.g. paginated fetches), set `concurrency` on the group to cap how many stages run at once instead of starting all of them immediately:
|
|
604
|
+
|
|
605
|
+
```js
|
|
606
|
+
{
|
|
607
|
+
key: "fetch-all-pages",
|
|
608
|
+
parallel: pageNumbers.map((n) => ({
|
|
609
|
+
key: `page-${n}`,
|
|
610
|
+
request: async () => fetchPage(n),
|
|
611
|
+
})),
|
|
612
|
+
concurrency: 5, // at most 5 requests in flight at a time
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Results land in `stageResults` under their own key regardless of `concurrency`, in the same shape as an unlimited group. With the `pipe()` builder: `.parallel(stages, { concurrency: 5 })`.
|
|
617
|
+
|
|
509
618
|
---
|
|
510
619
|
|
|
511
620
|
## Global middleware
|
|
@@ -600,25 +709,33 @@ const orchestrator = new PipelineOrchestrator({
|
|
|
600
709
|
/* ... */
|
|
601
710
|
],
|
|
602
711
|
metrics: {
|
|
603
|
-
onPipelineStart: ({ timestamp }) => {
|
|
604
|
-
console.log(
|
|
712
|
+
onPipelineStart: ({ timestamp, runId }) => {
|
|
713
|
+
console.log(`[${runId}] Pipeline started at`, new Date(timestamp).toISOString());
|
|
605
714
|
},
|
|
606
|
-
onPipelineEnd: ({ durationMs, success, stageResults }) => {
|
|
607
|
-
analytics.track("pipeline_complete", { durationMs, success });
|
|
715
|
+
onPipelineEnd: ({ durationMs, success, stageResults, runId }) => {
|
|
716
|
+
analytics.track("pipeline_complete", { durationMs, success, runId });
|
|
608
717
|
},
|
|
609
|
-
onStepDuration: ({ stepKey, durationMs, status }) => {
|
|
610
|
-
console.log(`[${stepKey}] ${status} in ${durationMs}ms`);
|
|
718
|
+
onStepDuration: ({ stepKey, durationMs, status, runId }) => {
|
|
719
|
+
console.log(`[${runId}] [${stepKey}] ${status} in ${durationMs}ms`);
|
|
611
720
|
},
|
|
612
721
|
},
|
|
613
722
|
},
|
|
614
723
|
});
|
|
615
724
|
```
|
|
616
725
|
|
|
617
|
-
| Callback | Receives
|
|
618
|
-
| ----------------- |
|
|
619
|
-
| `onPipelineStart` | `{ timestamp }`
|
|
620
|
-
| `onPipelineEnd` | `{ durationMs, success, stageResults }`
|
|
621
|
-
| `onStepDuration` | `{ stepKey, durationMs, status }`
|
|
726
|
+
| Callback | Receives | Description |
|
|
727
|
+
| ----------------- | ------------------------------------------------- | --------------------------------- |
|
|
728
|
+
| `onPipelineStart` | `{ timestamp, runId }` | Fires at the beginning of `run()` |
|
|
729
|
+
| `onPipelineEnd` | `{ durationMs, success, stageResults, runId }` | Fires when `run()` completes |
|
|
730
|
+
| `onStepDuration` | `{ stepKey, durationMs, status, runId }` | Fires after every executed step |
|
|
731
|
+
|
|
732
|
+
### Correlating a run (`runId`)
|
|
733
|
+
|
|
734
|
+
Every `run()` call generates a fresh `runId` (a UUID, or a timestamp-based fallback in environments without `crypto.randomUUID`), shared by all metrics callbacks, log entries (`getLogs()`), and step events (`PipelineStepEvent.runId`) produced during that run — including all attempts of `pipelineRetry`. `rerunStep()` generates its own separate `runId`. Use `orchestrator.getRunId()` to read the current/last one, or read `runId` off any event/log/metrics callback to correlate everything that happened during one execution in your logging/tracing backend:
|
|
735
|
+
|
|
736
|
+
```js
|
|
737
|
+
orchestrator.on("log", (entry) => sendToLogBackend({ ...entry, runId: orchestrator.getRunId() }));
|
|
738
|
+
```
|
|
622
739
|
|
|
623
740
|
---
|
|
624
741
|
|
|
@@ -671,12 +788,26 @@ const orchestrator = pipe()
|
|
|
671
788
|
| Builder method | Description |
|
|
672
789
|
| ----------------------------- | -------------------------------------------------------- |
|
|
673
790
|
| `.step(stage)` | Add a sequential stage |
|
|
674
|
-
| `.parallel(stages, options?)` | Add a parallel group (`key`
|
|
791
|
+
| `.parallel(stages, options?)` | Add a parallel group (`key`/`concurrency` optional, see [Limiting concurrency](#limiting-concurrency)) |
|
|
675
792
|
| `.subPipeline(item)` | Embed a sub-pipeline as a stage |
|
|
676
793
|
| `.stream(stage)` | Add a stream stage (AsyncIterable) |
|
|
677
794
|
| `.build(options?)` | Create and return a `PipelineOrchestrator` |
|
|
678
795
|
| `.toConfig(options?)` | Return `PipelineConfig` without creating an orchestrator |
|
|
679
796
|
|
|
797
|
+
#### Typed chaining (TypeScript)
|
|
798
|
+
|
|
799
|
+
In TypeScript, `pipe().step(...)` tracks the type of `prev` across the chain: each `.step()`'s `prev` is typed as the previous step's return value (`undefined` for the very first step, matching the orchestrator's actual runtime behavior). `.parallel()` / `.subPipeline()` / `.stream()` don't change it — exactly like at runtime, where `prev` for the next step still comes from the last regular `.step()`, not from a parallel group's results:
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
const orchestrator = pipe()
|
|
803
|
+
.step({ key: "auth", request: async (): Promise<string> => getToken() })
|
|
804
|
+
.step({ key: "fetchUser", request: async ({ prev }) => fetchUser(prev) }) // prev: string — inferred, autocompletes
|
|
805
|
+
.step({ key: "oops", request: async ({ prev }) => prev.totallyNotAMethod() }) // ✗ compile error: wrong type for prev
|
|
806
|
+
.build();
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
This works whether or not you keep reassigning the chain (`builder.step(...)` without capturing the return value still mutates the same instance, just like before) — the typing is purely additive and doesn't change runtime behavior.
|
|
810
|
+
|
|
680
811
|
---
|
|
681
812
|
|
|
682
813
|
## validatePipelineConfig()
|
|
@@ -848,6 +979,8 @@ type HttpAdapter = {
|
|
|
848
979
|
};
|
|
849
980
|
```
|
|
850
981
|
|
|
982
|
+
When `adapter` is set, `createRestClient()` never calls `axios.create()` — the built-in axios instance simply isn't constructed, so adapter-only usage (e.g. in Cloudflare Workers / Deno) doesn't pay for it.
|
|
983
|
+
|
|
851
984
|
---
|
|
852
985
|
|
|
853
986
|
## Vue integration
|
|
@@ -989,7 +1122,8 @@ rest-pipeline-js
|
|
|
989
1122
|
│ ├── RequestExecutor — retry + backoff + Retry-After + AbortController timeout
|
|
990
1123
|
│ ├── CacheManager — in-memory TTL cache for GET responses
|
|
991
1124
|
│ ├── RateLimiter — concurrency + req/interval sliding window
|
|
992
|
-
│ ├──
|
|
1125
|
+
│ ├── CircuitBreaker — closed → open → half-open; rejects locally when open
|
|
1126
|
+
│ ├── AuthProvider — Bearer injection; 401 refresh + one retry; optional tokenTtlMs cache
|
|
993
1127
|
│ ├── MetricsCollector — onRequestStart / onRequestEnd callbacks
|
|
994
1128
|
│ ├── HeaderSanitizer — masks sensitive headers before metrics callbacks
|
|
995
1129
|
│ └── HttpAdapter — pluggable transport (default: axios; swap for fetch / edge)
|
package/dist/cjs/cache.js
CHANGED
|
@@ -64,6 +64,21 @@ class TtlCache {
|
|
|
64
64
|
delete(key) {
|
|
65
65
|
this.store.delete(key);
|
|
66
66
|
}
|
|
67
|
+
/** Итератор по всем ключам кэша (включая потенциально устаревшие — не фильтрует по TTL). */
|
|
68
|
+
keys() {
|
|
69
|
+
return this.store.keys();
|
|
70
|
+
}
|
|
71
|
+
/** Удаляет все записи, для которых predicate(key) вернул true. Возвращает количество удалённых записей. */
|
|
72
|
+
deleteWhere(predicate) {
|
|
73
|
+
let count = 0;
|
|
74
|
+
for (const key of [...this.store.keys()]) {
|
|
75
|
+
if (predicate(key)) {
|
|
76
|
+
this.store.delete(key);
|
|
77
|
+
count++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return count;
|
|
81
|
+
}
|
|
67
82
|
clear() {
|
|
68
83
|
this.store.clear();
|
|
69
84
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CircuitBreaker = exports.CircuitOpenError = void 0;
|
|
4
|
+
/** Ошибка, бросаемая вместо реального запроса, когда circuit breaker открыт. */
|
|
5
|
+
class CircuitOpenError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Circuit breaker is open — request rejected without calling the network");
|
|
8
|
+
this.code = "CIRCUIT_OPEN";
|
|
9
|
+
this.name = "CircuitOpenError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.CircuitOpenError = CircuitOpenError;
|
|
13
|
+
/**
|
|
14
|
+
* Простой circuit breaker (closed → open → half-open → closed) для HTTP-клиента.
|
|
15
|
+
* - closed: запросы выполняются как обычно, считаются последовательные ошибки.
|
|
16
|
+
* - open: запросы немедленно отклоняются (CircuitOpenError), без обращения к сети.
|
|
17
|
+
* - half-open: после openMs пропускает запросы "на пробу"; успех закрывает circuit,
|
|
18
|
+
* неудача снова открывает его.
|
|
19
|
+
*/
|
|
20
|
+
class CircuitBreaker {
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.state = "closed";
|
|
24
|
+
this.failureCount = 0;
|
|
25
|
+
this.successCount = 0;
|
|
26
|
+
this.openedAt = 0;
|
|
27
|
+
}
|
|
28
|
+
/** Текущее состояние (учитывает автоматический переход open → half-open по таймауту). */
|
|
29
|
+
getState() {
|
|
30
|
+
this._maybeTransitionToHalfOpen();
|
|
31
|
+
return this.state;
|
|
32
|
+
}
|
|
33
|
+
/** Можно ли выполнить запрос сейчас (false, если circuit открыт). */
|
|
34
|
+
canExecute() {
|
|
35
|
+
this._maybeTransitionToHalfOpen();
|
|
36
|
+
return this.state !== "open";
|
|
37
|
+
}
|
|
38
|
+
/** Зарегистрировать успешный запрос. */
|
|
39
|
+
onSuccess() {
|
|
40
|
+
var _a;
|
|
41
|
+
if (this.state === "half-open") {
|
|
42
|
+
this.successCount++;
|
|
43
|
+
const needed = (_a = this.config.successThreshold) !== null && _a !== void 0 ? _a : 1;
|
|
44
|
+
if (this.successCount >= needed) {
|
|
45
|
+
this._close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.failureCount = 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Зарегистрировать неудачный запрос (error уже приведена к ApiError). */
|
|
53
|
+
onFailure(error) {
|
|
54
|
+
if (this.config.isFailure && !this.config.isFailure(error))
|
|
55
|
+
return;
|
|
56
|
+
if (this.state === "half-open") {
|
|
57
|
+
this._open();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.failureCount++;
|
|
61
|
+
if (this.failureCount >= this.config.failureThreshold) {
|
|
62
|
+
this._open();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
_maybeTransitionToHalfOpen() {
|
|
66
|
+
if (this.state === "open" &&
|
|
67
|
+
Date.now() - this.openedAt >= this.config.openMs) {
|
|
68
|
+
this.state = "half-open";
|
|
69
|
+
this.successCount = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
_open() {
|
|
73
|
+
this.state = "open";
|
|
74
|
+
this.openedAt = Date.now();
|
|
75
|
+
this.failureCount = 0;
|
|
76
|
+
this.successCount = 0;
|
|
77
|
+
}
|
|
78
|
+
_close() {
|
|
79
|
+
this.state = "closed";
|
|
80
|
+
this.failureCount = 0;
|
|
81
|
+
this.successCount = 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.CircuitBreaker = CircuitBreaker;
|
package/dist/cjs/index.js
CHANGED
|
@@ -23,3 +23,4 @@ __exportStar(require("./progress-tracker"), exports);
|
|
|
23
23
|
__exportStar(require("./pipeline-orchestrator"), exports);
|
|
24
24
|
__exportStar(require("./pipeline-builder"), exports);
|
|
25
25
|
__exportStar(require("./pipeline-validator"), exports);
|
|
26
|
+
__exportStar(require("./circuit-breaker"), exports);
|
|
@@ -36,10 +36,19 @@ function createPipeline(stages, options = {}) {
|
|
|
36
36
|
* Fluent builder для создания pipeline.
|
|
37
37
|
* Позволяет строить конвейер цепочкой вызовов вместо ручного конструирования массива stages.
|
|
38
38
|
*
|
|
39
|
+
* `TPrev` — тип `prev`, который получит *следующий* `.step()` (тип данных, возвращённых
|
|
40
|
+
* текущим шагом). Это чисто типовой (phantom) параметр — во время выполнения класс всегда
|
|
41
|
+
* работает с одним и тем же массивом stages, поведение не меняется по сравнению с
|
|
42
|
+
* нетипизированным использованием (без чейнинга — через отдельные вызовы без переприсвоения).
|
|
43
|
+
*
|
|
44
|
+
* `.parallel()` / `.subPipeline()` / `.stream()` не меняют `TPrev` — это соответствует
|
|
45
|
+
* реальному поведению orchestrator: `prev` следующего шага берётся из последнего обычного
|
|
46
|
+
* (`step`) шага, а не из параллельной группы/sub-pipeline/стрима.
|
|
47
|
+
*
|
|
39
48
|
* @example
|
|
40
49
|
* const orchestrator = pipe()
|
|
41
|
-
* .step({ key: "auth", request: async () => getToken() })
|
|
42
|
-
* .step({ key: "fetchUser",
|
|
50
|
+
* .step({ key: "auth", request: async () => getToken() }) // TPrev для следующего шага: string
|
|
51
|
+
* .step({ key: "fetchUser", request: async ({ prev }) => fetchUser(prev) }) // prev: string — автокомплит и проверка типов
|
|
43
52
|
* .parallel([
|
|
44
53
|
* { key: "loadA", request: async () => loadA() },
|
|
45
54
|
* { key: "loadB", request: async () => loadB() },
|
|
@@ -52,14 +61,19 @@ class PipelineBuilder {
|
|
|
52
61
|
}
|
|
53
62
|
/**
|
|
54
63
|
* Добавить обычный (последовательный) шаг.
|
|
64
|
+
* `prev` в этом шаге типизируется как результат предыдущего `.step()` (или `undefined` для первого).
|
|
65
|
+
* Тип `TOutput` обычно выводится автоматически из возвращаемого значения `request`/`after`.
|
|
55
66
|
*/
|
|
56
67
|
step(stage) {
|
|
57
68
|
this.stages.push(stage);
|
|
69
|
+
// Безопасный cast: TPrev/TOutput — чисто типовые параметры, не хранятся в экземпляре,
|
|
70
|
+
// поэтому смена фантомного типа не требует создания нового объекта.
|
|
58
71
|
return this;
|
|
59
72
|
}
|
|
60
73
|
/**
|
|
61
74
|
* Добавить группу параллельных шагов.
|
|
62
|
-
* Все шаги в группе выполняются одновременно через Promise.all
|
|
75
|
+
* Все шаги в группе выполняются одновременно через Promise.all (либо через пул,
|
|
76
|
+
* если задан `concurrency`).
|
|
63
77
|
*/
|
|
64
78
|
parallel(stages, options) {
|
|
65
79
|
var _a;
|
|
@@ -69,6 +83,9 @@ class PipelineBuilder {
|
|
|
69
83
|
...((options === null || options === void 0 ? void 0 : options.continueOnError) !== undefined
|
|
70
84
|
? { continueOnError: options.continueOnError }
|
|
71
85
|
: {}),
|
|
86
|
+
...((options === null || options === void 0 ? void 0 : options.concurrency) !== undefined
|
|
87
|
+
? { concurrency: options.concurrency }
|
|
88
|
+
: {}),
|
|
72
89
|
};
|
|
73
90
|
this.stages.push(group);
|
|
74
91
|
return this;
|
|
@@ -110,6 +127,8 @@ exports.PipelineBuilder = PipelineBuilder;
|
|
|
110
127
|
/**
|
|
111
128
|
* Создаёт новый PipelineBuilder.
|
|
112
129
|
* Точка входа для fluent API.
|
|
130
|
+
* `prev` первого `.step()` типизируется как `undefined` — ровно так, как ведёт себя
|
|
131
|
+
* orchestrator в реальности (у первого шага pipeline нет предыдущего результата).
|
|
113
132
|
*
|
|
114
133
|
* @example
|
|
115
134
|
* const orchestrator = pipe()
|