rest-pipeline-js 1.3.6 → 1.3.8

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +269 -0
  3. package/dist/cjs/index.js +2 -0
  4. package/dist/cjs/pipeline-builder.js +121 -0
  5. package/dist/cjs/pipeline-orchestrator.js +215 -23
  6. package/dist/cjs/pipeline-validator.js +113 -0
  7. package/dist/cjs/react.js +3 -1
  8. package/dist/cjs/rest-client.js +34 -21
  9. package/dist/cjs/usePipelineStageResult-react.js +28 -0
  10. package/dist/cjs/usePipelineStageResult-vue.js +29 -0
  11. package/dist/cjs/vue.js +3 -1
  12. package/dist/esm/index.d.ts +2 -0
  13. package/dist/esm/index.js +2 -0
  14. package/dist/esm/pipeline-builder.d.ts +84 -0
  15. package/dist/esm/pipeline-builder.js +115 -0
  16. package/dist/esm/pipeline-orchestrator.d.ts +23 -4
  17. package/dist/esm/pipeline-orchestrator.js +215 -23
  18. package/dist/esm/pipeline-validator.d.ts +17 -0
  19. package/dist/esm/pipeline-validator.js +110 -0
  20. package/dist/esm/react.d.ts +1 -0
  21. package/dist/esm/react.js +1 -0
  22. package/dist/esm/rest-client.js +34 -21
  23. package/dist/esm/types.d.ts +126 -2
  24. package/dist/esm/usePipelineStageResult-react.d.ts +16 -0
  25. package/dist/esm/usePipelineStageResult-react.js +25 -0
  26. package/dist/esm/usePipelineStageResult-vue.d.ts +17 -0
  27. package/dist/esm/usePipelineStageResult-vue.js +26 -0
  28. package/dist/esm/usePipelineStepEvents-react.d.ts +1 -1
  29. package/dist/esm/usePipelineStepEvents-vue.d.ts +1 -1
  30. package/dist/esm/vue.d.ts +1 -0
  31. package/dist/esm/vue.js +1 -0
  32. package/package.json +3 -2
  33. package/src/index.ts +2 -0
  34. package/src/pipeline-builder.ts +158 -0
  35. package/src/pipeline-orchestrator.ts +236 -16
  36. package/src/pipeline-validator.ts +151 -0
  37. package/src/react.ts +1 -0
  38. package/src/rest-client.ts +34 -26
  39. package/src/types.ts +171 -2
  40. package/src/usePipelineStageResult-react.ts +32 -0
  41. package/src/usePipelineStageResult-vue.ts +34 -0
  42. package/src/vue-demo/demo.css +768 -38
  43. package/src/vue-demo/demo.vue +558 -109
  44. package/src/vue-demo/index.html +21 -12
  45. package/src/vue.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.7] - 2026-04-04
4
+
5
+ ### Added
6
+
7
+ #### Pipeline Orchestrator
8
+
9
+ - **Pipeline metrics** — `PipelineConfig.metrics` with three callbacks:
10
+ - `onPipelineStart({ timestamp })` — fires at the beginning of `run()`
11
+ - `onPipelineEnd({ durationMs, success, stageResults })` — fires when `run()` completes
12
+ - `onStepDuration({ stepKey, durationMs, status })` — fires after every executed step
13
+ - **Plugin system** — `options.plugins` accepts an array of `PipelinePlugin` objects. Each plugin receives the orchestrator instance in `install(orchestrator)` and can subscribe to events, add middleware hooks, etc. Returning a function from `install()` registers it as a cleanup callback.
14
+ - **`destroy()`** — new public method that invokes cleanup functions from all installed plugins.
15
+ - **Persist adapter** — `options.persistAdapter` accepts a `PipelineStateAdapter` object with `save` / `load` methods. When set: state is automatically loaded at the start of `run()` (via `importState`) and saved after each successfully completed step.
16
+ - **Stream stages** — new `StreamStageConfig` element type for `PipelineItem`. The `stream` function returns an `AsyncIterable<T>`; the orchestrator iterates it and collects chunks into an array (the stage result). The optional `onChunk(chunk, sharedData)` callback fires for each chunk in real time. Stream stages honour `abort()`, `continueOnError`, and emit standard step events.
17
+ - **Generic step keys** — `PipelineOrchestrator<TKeys extends string = string>` now accepts a generic type parameter for typed auto-complete in `on()`, `rerunStep()`, and `subscribeStepProgress()`.
18
+
19
+ #### DX utilities
20
+
21
+ - **`createPipeline(stages, options?)`** — factory function that creates a `PipelineOrchestrator` without the nested `{ config: { stages } }` boilerplate.
22
+ - **`pipe()`** — fluent builder API. Methods: `.step()`, `.parallel()`, `.subPipeline()`, `.stream()`, `.build(options?)`, `.toConfig(options?)`.
23
+ - **`validatePipelineConfig(config, context?)`** — validates a `PipelineConfig` before runtime. Checks for duplicate keys, empty keys, empty `stages` array, invalid field types, and recursively validates nested sub-pipelines. Returns `{ valid: boolean; errors: string[] }`.
24
+ - **`getStageResults()`** — synchronous snapshot of all stage results (no subscription needed).
25
+
26
+ #### Vue / React hooks
27
+
28
+ - **`usePipelineStageResultVue(orchestrator, stepKey)`** — reactive `Ref<PipelineStepResult | null>` for a single step.
29
+ - **`usePipelineStageResultReact(orchestrator, stepKey)`** — state hook for a single step, updates on every `stageResults` change.
30
+
31
+ #### RestClient
32
+
33
+ - **`HttpAdapter`** — new `adapter` field in `HttpConfig`. When provided, replaces the built-in axios client with a custom implementation (e.g. native `fetch`). All other features (auth, interceptors, retry, sanitization, metrics) continue to work on top of the adapter.
34
+
35
+ #### Types
36
+
37
+ - `PipelineMetrics` interface
38
+ - `PipelinePlugin` type
39
+ - `PipelineStateAdapter` type
40
+ - `StreamStageConfig<T>` type; updated `PipelineItem` union to include it
41
+ - `HttpAdapter` type
42
+ - `PipelineLogEventType` union — exhaustive list of all log event type strings
43
+ - Extended `PipelineConfig` with `metrics?`
44
+ - Extended `PipelineOptions` with `persistAdapter?` and `plugins?`
45
+ - Extended `HttpConfig` with `adapter?`
46
+
47
+ ---
48
+
3
49
  ## [1.3.6] - 2026-04-03
4
50
 
5
51
  ### Added
package/README.md CHANGED
@@ -161,6 +161,7 @@ Creates a REST client with advanced HTTP features.
161
161
  | `sanitizeHeaders` | Mask sensitive headers in metrics callbacks (default: `false`) |
162
162
  | `sensitiveHeaders` | Additional headers to mask (extends `DEFAULT_SENSITIVE_HEADERS`) |
163
163
  | `retry.maxRetryAfterMs` | Max wait from `Retry-After` header in ms (default: `60000`) |
164
+ | `adapter` | Custom HTTP adapter (e.g. native `fetch`) — replaces built-in axios |
164
165
 
165
166
  **Per-request cache override:**
166
167
 
@@ -290,6 +291,8 @@ new PipelineOrchestrator({
290
291
  | `isPaused()` | Check if pipeline is paused |
291
292
  | `exportState()` | Serialize stageResults and logs to a plain object |
292
293
  | `importState(state)` | Restore stageResults and logs from a snapshot |
294
+ | `getStageResults()` | Synchronous snapshot of all stage results (no subscription needed) |
295
+ | `destroy()` | Run cleanup callbacks from all installed plugins |
293
296
  | `subscribeProgress(listener)` | Subscribe to progress updates |
294
297
  | `subscribeStageResults(listener)` | Subscribe to stageResults changes |
295
298
  | `subscribeStepProgress(stepKey, listener)` | Subscribe to a specific stage's progress |
@@ -457,6 +460,255 @@ console.log(orchestrator2.getLogs()); // restored logs (timestamps as Date objec
457
460
 
458
461
  ---
459
462
 
463
+ ### Pipeline metrics
464
+
465
+ Observe pipeline execution without modifying stage logic:
466
+
467
+ ```js
468
+ const orchestrator = new PipelineOrchestrator({
469
+ config: {
470
+ stages: [ /* ... */ ],
471
+ metrics: {
472
+ onPipelineStart: ({ timestamp }) => {
473
+ console.log("Pipeline started at", new Date(timestamp).toISOString());
474
+ },
475
+ onPipelineEnd: ({ durationMs, success, stageResults }) => {
476
+ analytics.track("pipeline_complete", { durationMs, success });
477
+ },
478
+ onStepDuration: ({ stepKey, durationMs, status }) => {
479
+ console.log(`[${stepKey}] ${status} in ${durationMs}ms`);
480
+ },
481
+ },
482
+ },
483
+ });
484
+ ```
485
+
486
+ | Callback | Receives | Description |
487
+ |----------|----------|-------------|
488
+ | `onPipelineStart` | `{ timestamp }` | Fires at the beginning of `run()` |
489
+ | `onPipelineEnd` | `{ durationMs, success, stageResults }` | Fires when `run()` completes |
490
+ | `onStepDuration` | `{ stepKey, durationMs, status }` | Fires after every executed step |
491
+
492
+ ---
493
+
494
+ ### createPipeline() + pipe() builder
495
+
496
+ #### createPipeline() — short factory
497
+
498
+ ```js
499
+ import { createPipeline } from "rest-pipeline-js";
500
+
501
+ const orchestrator = createPipeline(
502
+ [
503
+ { key: "fetchUser", request: async () => fetchUser() },
504
+ { key: "process", request: async ({ prev }) => process(prev) },
505
+ ],
506
+ {
507
+ httpConfig: { baseURL: "https://api.example.com" },
508
+ sharedData: { userId: 42 },
509
+ pipelineOptions: { continueOnError: false },
510
+ metrics: { onStepDuration: ({ stepKey, durationMs }) => console.log(stepKey, durationMs) },
511
+ },
512
+ );
513
+ ```
514
+
515
+ #### pipe() — fluent builder
516
+
517
+ ```js
518
+ import { pipe } from "rest-pipeline-js";
519
+
520
+ const orchestrator = pipe()
521
+ .step({ key: "auth", request: async () => getToken() })
522
+ .step({ key: "fetchUser", request: async ({ prev }) => fetchUser(prev) })
523
+ .parallel([
524
+ { key: "loadPosts", request: async () => fetchPosts() },
525
+ { key: "loadNotifs", request: async () => fetchNotifications() },
526
+ ])
527
+ .stream({
528
+ key: "liveUpdates",
529
+ stream: async function* () { yield* subscribe("/events"); },
530
+ onChunk: (chunk) => updateUI(chunk),
531
+ })
532
+ .build({ httpConfig: { baseURL: "https://api.example.com" } });
533
+ ```
534
+
535
+ | Builder method | Description |
536
+ |----------------|-------------|
537
+ | `.step(stage)` | Add a sequential stage |
538
+ | `.parallel(stages, options?)` | Add a parallel group (`key` auto-generated if omitted) |
539
+ | `.subPipeline(item)` | Embed a sub-pipeline as a stage |
540
+ | `.stream(stage)` | Add a stream stage (AsyncIterable) |
541
+ | `.build(options?)` | Create and return a `PipelineOrchestrator` |
542
+ | `.toConfig(options?)` | Return `PipelineConfig` without creating an orchestrator |
543
+
544
+ ---
545
+
546
+ ### validatePipelineConfig()
547
+
548
+ Catch configuration errors before runtime:
549
+
550
+ ```js
551
+ import { validatePipelineConfig } from "rest-pipeline-js";
552
+
553
+ const { valid, errors } = validatePipelineConfig({
554
+ stages: [
555
+ { key: "step1", request: async () => data },
556
+ { key: "step1", request: async () => other }, // duplicate!
557
+ { key: "", request: async () => other }, // empty key!
558
+ ],
559
+ });
560
+
561
+ if (!valid) console.error(errors);
562
+ // ["[root] duplicate stage key: "step1"", "[root] stage key must be a non-empty string"]
563
+ ```
564
+
565
+ Validates: duplicate keys, empty/invalid keys, empty `stages` array, invalid field types (`request`, `condition`, `retryCount`, `timeoutMs`), and recursively validates nested `subPipeline` configs.
566
+
567
+ ---
568
+
569
+ ### Plugin system
570
+
571
+ Package reusable orchestrator behavior into plugins:
572
+
573
+ ```js
574
+ const loggingPlugin = {
575
+ name: "logging",
576
+ install(orchestrator) {
577
+ const off = orchestrator.on("log", (event) => {
578
+ if (event.type === "step:success") console.log("✓", event.stepKey);
579
+ if (event.type === "step:error") console.error("✗", event.stepKey, event.error);
580
+ });
581
+ return () => off(); // cleanup on orchestrator.destroy()
582
+ },
583
+ };
584
+
585
+ const orchestrator = new PipelineOrchestrator({
586
+ config: {
587
+ stages: [ /* ... */ ],
588
+ options: {
589
+ plugins: [loggingPlugin, analyticsPlugin],
590
+ },
591
+ },
592
+ });
593
+
594
+ // Call when the orchestrator is no longer needed:
595
+ orchestrator.destroy();
596
+ ```
597
+
598
+ - `install(orchestrator)` — receives the orchestrator instance; may subscribe to events, set up middleware, etc.
599
+ - If `install` returns a function, it is registered as a cleanup callback and invoked by `destroy()`.
600
+
601
+ ---
602
+
603
+ ### Persist adapter
604
+
605
+ Automatically save and restore pipeline state across page reloads:
606
+
607
+ ```js
608
+ const localStorageAdapter = {
609
+ save: (state) => localStorage.setItem("pipeline", JSON.stringify(state)),
610
+ load: () => {
611
+ const raw = localStorage.getItem("pipeline");
612
+ return raw ? JSON.parse(raw) : null;
613
+ },
614
+ };
615
+
616
+ const orchestrator = new PipelineOrchestrator({
617
+ config: {
618
+ stages: [ /* ... */ ],
619
+ options: { persistAdapter: localStorageAdapter },
620
+ },
621
+ });
622
+
623
+ // run() loads saved state at start; saves after each completed step
624
+ await orchestrator.run();
625
+ ```
626
+
627
+ The adapter interface:
628
+
629
+ ```ts
630
+ type PipelineStateAdapter = {
631
+ save(state: PipelineExportedState): void | Promise<void>;
632
+ load(): PipelineExportedState | null | Promise<PipelineExportedState | null>;
633
+ };
634
+ ```
635
+
636
+ Both methods may be async (useful for IndexedDB or remote storage).
637
+
638
+ ---
639
+
640
+ ### Stream stages (SSE / AsyncIterable)
641
+
642
+ 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.
643
+
644
+ ```js
645
+ const orchestrator = createPipeline([
646
+ { key: "auth", request: async () => getToken() },
647
+ {
648
+ key: "liveData",
649
+ stream: async function* ({ prev }) {
650
+ // prev is the result of "auth"
651
+ const source = new EventSource(`/api/stream?token=${prev}`);
652
+ yield* eventSourceToAsyncIterable(source);
653
+ },
654
+ onChunk: (chunk, sharedData) => {
655
+ sharedData.partial = (sharedData.partial ?? "") + chunk;
656
+ updateUI(sharedData.partial);
657
+ },
658
+ },
659
+ {
660
+ key: "finalize",
661
+ // allResults.liveData.data is the full array of chunks
662
+ request: async ({ allResults }) => allResults.liveData.data.join(""),
663
+ },
664
+ ]);
665
+ ```
666
+
667
+ - Respects `abort()` — checks the abort signal between each chunk.
668
+ - Supports `continueOnError` — failed stream stages can be skipped like any other step.
669
+ - Emits standard step events: `step:start`, `step:success`, `step:error`.
670
+
671
+ ---
672
+
673
+ ### HTTP Adapter (custom fetch / edge environments)
674
+
675
+ Replace the built-in axios client with any HTTP implementation:
676
+
677
+ ```js
678
+ const fetchAdapter = {
679
+ async request(config) {
680
+ const url = `${config.baseURL ?? ""}${config.url ?? ""}`;
681
+ const res = await fetch(url, {
682
+ method: config.method ?? "GET",
683
+ body: config.data ? JSON.stringify(config.data) : undefined,
684
+ headers: { "Content-Type": "application/json", ...config.headers },
685
+ signal: config.signal,
686
+ });
687
+ const data = await res.json();
688
+ return { data, status: res.status, statusText: res.statusText,
689
+ headers: Object.fromEntries(res.headers.entries()) };
690
+ },
691
+ };
692
+
693
+ const client = createRestClient({
694
+ baseURL: "https://api.example.com",
695
+ adapter: fetchAdapter,
696
+ // Auth, interceptors, sanitizeHeaders, metrics still work on top of the adapter
697
+ auth: { getToken: async () => token },
698
+ interceptors: { request: [addCorrelationId] },
699
+ });
700
+ ```
701
+
702
+ ```ts
703
+ type HttpAdapter = {
704
+ request<T = unknown>(
705
+ config: RestRequestConfig & { baseURL?: string },
706
+ ): Promise<ApiResponse<T>>;
707
+ };
708
+ ```
709
+
710
+ ---
711
+
460
712
  ### Vue integration
461
713
 
462
714
  #### Example: use in Vue component
@@ -504,6 +756,7 @@ Composition functions (import from `rest-pipeline-js/vue`):
504
756
  | `usePipelineLogsVue(orchestrator)` | `Ref<log[]>` | Reactive logs |
505
757
  | `useRerunPipelineStepVue(orchestrator)` | `function` | Bound `rerunStep` |
506
758
  | `useRestClientVue(config)` | `ComputedRef<RestClient>` | Reactive REST client |
759
+ | `usePipelineStageResultVue(orchestrator, stepKey)` | `Ref<PipelineStepResult \| null>` | Reactive result of a single stage |
507
760
 
508
761
  ---
509
762
 
@@ -560,6 +813,7 @@ Hooks (import from `rest-pipeline-js/react`):
560
813
  | `usePipelineLogsReact(orchestrator)` | `log[]` | Reactive logs |
561
814
  | `useRerunPipelineStepReact(orchestrator)` | `function` | Bound `rerunStep` |
562
815
  | `useRestClientReact(config)` | `RestClient` | Memoized REST client |
816
+ | `usePipelineStageResultReact(orchestrator, stepKey)` | `PipelineStepResult \| null` | Result of a single stage |
563
817
 
564
818
  ---
565
819
 
@@ -596,6 +850,21 @@ import {
596
850
 
597
851
  ---
598
852
 
853
+ ## Vue Demo
854
+
855
+ 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.
856
+
857
+ ```bash
858
+ git clone https://github.com/macrulezru/pipeline-js.git
859
+ cd pipeline-js
860
+ npm install
861
+ npm run demo:vue
862
+ ```
863
+
864
+ 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.
865
+
866
+ ---
867
+
599
868
  ## Development
600
869
 
601
870
  ```bash
package/dist/cjs/index.js CHANGED
@@ -21,3 +21,5 @@ __exportStar(require("./request-executor"), exports);
21
21
  __exportStar(require("./error-handler"), exports);
22
22
  __exportStar(require("./progress-tracker"), exports);
23
23
  __exportStar(require("./pipeline-orchestrator"), exports);
24
+ __exportStar(require("./pipeline-builder"), exports);
25
+ __exportStar(require("./pipeline-validator"), exports);
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PipelineBuilder = void 0;
4
+ exports.createPipeline = createPipeline;
5
+ exports.pipe = pipe;
6
+ const pipeline_orchestrator_1 = require("./pipeline-orchestrator");
7
+ /**
8
+ * Сокращённая фабричная функция для создания PipelineOrchestrator.
9
+ * Избавляет от необходимости писать вложенный объект `{ config: { stages: [...] } }`.
10
+ *
11
+ * @example
12
+ * const orchestrator = createPipeline([
13
+ * { key: "fetchUser", request: async () => fetchUser() },
14
+ * { key: "processData", request: async ({ prev }) => process(prev) },
15
+ * ], {
16
+ * httpConfig: { baseURL: "https://api.example.com" },
17
+ * sharedData: { userId: 42 },
18
+ * });
19
+ */
20
+ function createPipeline(stages, options = {}) {
21
+ return new pipeline_orchestrator_1.PipelineOrchestrator({
22
+ config: {
23
+ stages,
24
+ middleware: options.middleware,
25
+ options: options.pipelineOptions,
26
+ metrics: options.metrics,
27
+ },
28
+ httpConfig: options.httpConfig,
29
+ sharedData: options.sharedData,
30
+ });
31
+ }
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // 4.2 pipe() — Fluent builder API
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ /**
36
+ * Fluent builder для создания pipeline.
37
+ * Позволяет строить конвейер цепочкой вызовов вместо ручного конструирования массива stages.
38
+ *
39
+ * @example
40
+ * const orchestrator = pipe()
41
+ * .step({ key: "auth", request: async () => getToken() })
42
+ * .step({ key: "fetchUser", condition: ({ prev }) => !!prev, request: async ({ prev }) => fetchUser(prev) })
43
+ * .parallel([
44
+ * { key: "loadA", request: async () => loadA() },
45
+ * { key: "loadB", request: async () => loadB() },
46
+ * ])
47
+ * .build({ httpConfig: { baseURL: "https://api.example.com" } });
48
+ */
49
+ class PipelineBuilder {
50
+ constructor() {
51
+ this.stages = [];
52
+ }
53
+ /**
54
+ * Добавить обычный (последовательный) шаг.
55
+ */
56
+ step(stage) {
57
+ this.stages.push(stage);
58
+ return this;
59
+ }
60
+ /**
61
+ * Добавить группу параллельных шагов.
62
+ * Все шаги в группе выполняются одновременно через Promise.all.
63
+ */
64
+ parallel(stages, options) {
65
+ var _a;
66
+ const group = {
67
+ key: (_a = options === null || options === void 0 ? void 0 : options.key) !== null && _a !== void 0 ? _a : `parallel-${this.stages.length}`,
68
+ parallel: stages,
69
+ ...((options === null || options === void 0 ? void 0 : options.continueOnError) !== undefined
70
+ ? { continueOnError: options.continueOnError }
71
+ : {}),
72
+ };
73
+ this.stages.push(group);
74
+ return this;
75
+ }
76
+ /**
77
+ * Добавить вложенный pipeline как шаг.
78
+ */
79
+ subPipeline(item) {
80
+ this.stages.push(item);
81
+ return this;
82
+ }
83
+ /**
84
+ * Добавить stream-шаг (SSE / AsyncIterable).
85
+ */
86
+ stream(stage) {
87
+ this.stages.push(stage);
88
+ return this;
89
+ }
90
+ /**
91
+ * Создать PipelineOrchestrator из накопленных шагов.
92
+ */
93
+ build(options = {}) {
94
+ return createPipeline([...this.stages], options);
95
+ }
96
+ /**
97
+ * Получить только конфиг (без создания orchestrator).
98
+ * Полезно для передачи конфига в другое место.
99
+ */
100
+ toConfig(options = {}) {
101
+ return {
102
+ stages: [...this.stages],
103
+ middleware: options.middleware,
104
+ options: options.pipelineOptions,
105
+ metrics: options.metrics,
106
+ };
107
+ }
108
+ }
109
+ exports.PipelineBuilder = PipelineBuilder;
110
+ /**
111
+ * Создаёт новый PipelineBuilder.
112
+ * Точка входа для fluent API.
113
+ *
114
+ * @example
115
+ * const orchestrator = pipe()
116
+ * .step({ key: "step1", request: async () => data })
117
+ * .build();
118
+ */
119
+ function pipe() {
120
+ return new PipelineBuilder();
121
+ }