rest-pipeline-js 1.2.6 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +665 -616
  2. package/dist/cjs/cache.js +48 -0
  3. package/dist/cjs/pipeline-orchestrator.js +407 -441
  4. package/dist/cjs/progress-tracker.js +3 -3
  5. package/dist/cjs/rate-limiter.js +68 -0
  6. package/dist/cjs/request-executor.js +88 -10
  7. package/dist/cjs/rest-client.js +84 -71
  8. package/dist/cjs/usePipelineRun-react.js +12 -5
  9. package/dist/cjs/usePipelineRun-vue.js +17 -8
  10. package/dist/esm/cache.d.ts +14 -0
  11. package/dist/esm/cache.js +44 -0
  12. package/dist/esm/pipeline-orchestrator.d.ts +51 -94
  13. package/dist/esm/pipeline-orchestrator.js +411 -445
  14. package/dist/esm/progress-tracker.d.ts +7 -3
  15. package/dist/esm/progress-tracker.js +3 -3
  16. package/dist/esm/rate-limiter.d.ts +19 -0
  17. package/dist/esm/rate-limiter.js +64 -0
  18. package/dist/esm/request-executor.d.ts +8 -2
  19. package/dist/esm/request-executor.js +88 -10
  20. package/dist/esm/rest-client.d.ts +10 -5
  21. package/dist/esm/rest-client.js +83 -38
  22. package/dist/esm/types.d.ts +76 -5
  23. package/dist/esm/usePipelineRun-react.d.ts +8 -4
  24. package/dist/esm/usePipelineRun-react.js +13 -6
  25. package/dist/esm/usePipelineRun-vue.d.ts +8 -5
  26. package/dist/esm/usePipelineRun-vue.js +18 -9
  27. package/dist/esm/useRestClient-react.d.ts +7 -5
  28. package/dist/esm/useRestClient-vue.d.ts +7 -5
  29. package/package.json +1 -1
  30. package/src/cache.ts +47 -0
  31. package/src/index.ts +8 -8
  32. package/src/pipeline-orchestrator.ts +534 -519
  33. package/src/progress-tracker.ts +3 -3
  34. package/src/rate-limiter.ts +76 -0
  35. package/src/request-executor.ts +103 -16
  36. package/src/rest-client.ts +106 -37
  37. package/src/types.ts +84 -5
  38. package/src/usePipelineRun-react.ts +52 -36
  39. package/src/usePipelineRun-vue.ts +63 -47
  40. package/tests/pipeline-orchestrator.test.ts +717 -126
  41. package/tests/request-executor.test.ts +39 -9
  42. package/tests/rest-client.test.ts +67 -9
  43. package/tests/types.test.ts +105 -20
  44. package/dist/error-handler.d.ts +0 -7
  45. package/dist/error-handler.js +0 -6
  46. package/dist/index.d.ts +0 -6
  47. package/dist/index.js +0 -7
  48. package/dist/pipeline-orchestrator.d.ts +0 -136
  49. package/dist/pipeline-orchestrator.js +0 -561
  50. package/dist/progress-tracker.d.ts +0 -22
  51. package/dist/progress-tracker.js +0 -43
  52. package/dist/react.d.ts +0 -5
  53. package/dist/react.js +0 -6
  54. package/dist/request-executor.d.ts +0 -9
  55. package/dist/request-executor.js +0 -29
  56. package/dist/rest-client.d.ts +0 -14
  57. package/dist/rest-client.js +0 -160
  58. package/dist/types.d.ts +0 -164
  59. package/dist/types.js +0 -1
  60. package/dist/usePipelineProgress-react.d.ts +0 -8
  61. package/dist/usePipelineProgress-react.js +0 -14
  62. package/dist/usePipelineProgress-vue.d.ts +0 -16
  63. package/dist/usePipelineProgress-vue.js +0 -14
  64. package/dist/usePipelineRun-react.d.ts +0 -12
  65. package/dist/usePipelineRun-react.js +0 -30
  66. package/dist/usePipelineRun-vue.d.ts +0 -21
  67. package/dist/usePipelineRun-vue.js +0 -42
  68. package/dist/usePipelineStepEvents-react.d.ts +0 -29
  69. package/dist/usePipelineStepEvents-react.js +0 -41
  70. package/dist/usePipelineStepEvents-vue.d.ts +0 -39
  71. package/dist/usePipelineStepEvents-vue.js +0 -40
  72. package/dist/useRestClient-react.d.ts +0 -15
  73. package/dist/useRestClient-react.js +0 -10
  74. package/dist/useRestClient-vue.d.ts +0 -15
  75. package/dist/useRestClient-vue.js +0 -10
  76. package/dist/vue.d.ts +0 -5
  77. package/dist/vue.js +0 -6
package/README.md CHANGED
@@ -23,287 +23,345 @@ const client = createRestClient({
23
23
  baseURL: "https://api.example.com",
24
24
  timeout: 5000,
25
25
  headers: { Authorization: "Bearer TOKEN" },
26
+ retry: { attempts: 2, delayMs: 500, backoffMultiplier: 2, retriableStatus: [429, 500, 503] },
27
+ cache: { enabled: true, ttlMs: 60000 },
28
+ rateLimit: { maxConcurrent: 3, maxRequestsPerInterval: 10, intervalMs: 1000 },
26
29
  });
27
30
 
28
- async function fetchUser(id) {
29
- const res = await client.request(`/users/${id}`);
30
- if (res.error) {
31
- console.error(res.error);
32
- } else {
33
- console.log(res.data);
34
- }
35
- }
31
+ const res = await client.get("/users/1");
32
+ console.log(res.data);
33
+
34
+ // PATCH support
35
+ await client.patch("/users/1", { name: "Alice" });
36
+
37
+ // Cancellable request
38
+ const req = client.cancellableRequest("my-key", "/search", { params: { q: "foo" } });
39
+ // Cancel it any time:
40
+ client.cancelRequest("my-key");
36
41
  ```
37
42
 
43
+ ---
44
+
38
45
  #### Example: Run a pipeline, handle errors, track progress, use shared data
39
46
 
40
47
  ```js
41
48
  import { PipelineOrchestrator } from "rest-pipeline-js";
42
49
 
43
- const pipelineConfig = {
44
- stages: [
45
- { key: "step1", command: "/api/step1", method: "POST" },
46
- {
47
- key: "step2",
48
- command: "/api/step2",
49
- method: "POST",
50
- dependsOn: ["step1"],
50
+ const orchestrator = new PipelineOrchestrator({
51
+ config: {
52
+ stages: [
53
+ {
54
+ key: "fetchUser",
55
+ request: async ({ sharedData }) => {
56
+ const res = await fetch(`/api/users/${sharedData.userId}`);
57
+ return res.json();
58
+ },
59
+ },
60
+ {
61
+ key: "processData",
62
+ condition: ({ prev }) => prev !== null,
63
+ before: ({ prev }) => ({ ...prev, processed: true }),
64
+ request: async ({ prev }) => prev,
65
+ after: ({ result }) => ({ ...result, finishedAt: Date.now() }),
66
+ },
67
+ ],
68
+ middleware: {
69
+ beforeEach: ({ stage }) => console.log("Starting:", stage.key),
70
+ afterEach: ({ stage, result }) => console.log("Done:", stage.key, result.data),
71
+ onError: ({ stage, error }) => console.error("Error in", stage.key, error),
51
72
  },
52
- ],
53
- };
54
- const httpConfig = {
55
- baseURL: "https://api.example.com",
56
- timeout: 7000,
57
- headers: { Authorization: "Bearer TOKEN" },
58
- retry: { attempts: 2, delayMs: 1000 },
59
- cache: { enabled: true, ttlMs: 60000 },
60
- rateLimit: { maxConcurrent: 2 },
61
- metrics: {
62
- onRequestStart: (info) => console.log("Start:", info),
63
- onRequestEnd: (info) => console.log("End:", info),
64
73
  },
65
- };
66
- const sharedData = { sessionId: "abc123" };
67
- const orchestrator = new PipelineOrchestrator(
68
- pipelineConfig,
69
- httpConfig,
70
- sharedData,
71
- { autoReset: true },
72
- );
74
+ httpConfig: {
75
+ baseURL: "https://api.example.com",
76
+ retry: { attempts: 2, delayMs: 1000, backoffMultiplier: 2 },
77
+ cache: { enabled: true, ttlMs: 60000 },
78
+ },
79
+ sharedData: { userId: 42 },
80
+ options: { autoReset: true },
81
+ });
73
82
 
74
83
  orchestrator.subscribeProgress((progress) => {
75
- console.log(
76
- "Current stage:",
77
- progress.currentStage,
78
- "Statuses:",
79
- progress.stageStatuses,
80
- );
84
+ console.log("Stage:", progress.currentStage, "Statuses:", progress.stageStatuses);
81
85
  });
82
- orchestrator.on("step:step1:success", (payload) => {
83
- console.log("Step 1 success:", payload.data);
84
- });
85
- orchestrator.on("step:step2:error", (payload) => {
86
- console.error("Step 2 error:", payload.error);
87
- });
88
- orchestrator.on("log", () => {
89
- console.log("Logs:", orchestrator.getLogs());
86
+
87
+ orchestrator.on("step:fetchUser:success", (payload) => {
88
+ console.log("fetchUser done:", payload.data);
90
89
  });
91
- orchestrator
92
- .run({ foo: "bar" })
93
- .then((result) => {
94
- console.log("Pipeline finished:", result);
95
- console.log("Stage results:", result.stageResults);
96
- })
97
- .catch((err) => {
98
- console.error("Pipeline error:", err);
99
- });
100
- // orchestrator.rerunStep('step2');
90
+
91
+ const result = await orchestrator.run();
92
+ console.log("Pipeline finished:", result.success);
93
+ console.log("Stage results:", result.stageResults);
101
94
  ```
102
95
 
103
96
  ---
104
97
 
105
- The module provides a universal mechanism for building and managing REST API pipelines with progress tracking, error handling, event subscriptions, and extensibility.
106
-
107
98
  ### Main classes and functions
108
99
 
109
100
  #### createRestClient(config: HttpConfig): RestClient
110
101
 
111
- Creates a REST client with advanced HTTP API features.
112
-
113
- #### Example
102
+ Creates a REST client with advanced HTTP features.
103
+
104
+ **Available methods:**
105
+
106
+ | Method | Description |
107
+ |--------|-------------|
108
+ | `get(url, config?)` | GET request |
109
+ | `post(url, data?, config?)` | POST request |
110
+ | `put(url, data?, config?)` | PUT request |
111
+ | `patch(url, data?, config?)` | PATCH request |
112
+ | `delete(url, config?)` | DELETE request |
113
+ | `request(url, config?)` | Generic request |
114
+ | `cancellableRequest(key, url, config?)` | Request cancellable by key |
115
+ | `cancelRequest(key)` | Cancel request by key |
116
+ | `clearCache()` | Clear this client's response cache |
117
+
118
+ **HttpConfig options:**
119
+
120
+ | Option | Description |
121
+ |--------|-------------|
122
+ | `baseURL` | Base URL for all requests |
123
+ | `timeout` | Request timeout in ms |
124
+ | `headers` | Default headers |
125
+ | `withCredentials` | Include cookies |
126
+ | `retry.attempts` | Number of retry attempts |
127
+ | `retry.delayMs` | Base delay between retries in ms |
128
+ | `retry.backoffMultiplier` | Exponential backoff multiplier |
129
+ | `retry.retriableStatus` | HTTP status codes eligible for retry (e.g. `[429, 500, 503]`) |
130
+ | `cache.enabled` | Enable response caching for GET requests |
131
+ | `cache.ttlMs` | Cache TTL in ms |
132
+ | `rateLimit.maxConcurrent` | Max simultaneous requests |
133
+ | `rateLimit.maxRequestsPerInterval` | Max requests per time window |
134
+ | `rateLimit.intervalMs` | Time window size in ms |
135
+ | `metrics.onRequestStart` | Callback on request start |
136
+ | `metrics.onRequestEnd` | Callback on request end (includes duration and bytes) |
137
+
138
+ **Per-request cache override:**
114
139
 
115
140
  ```js
116
- import { createRestClient } from "rest-pipeline-js";
117
- const client = createRestClient({
118
- baseURL: "https://api.example.com",
119
- timeout: 5000,
120
- headers: { Authorization: "Bearer TOKEN" },
121
- retry: { attempts: 2 },
122
- cache: { enabled: true, ttlMs: 60000 },
141
+ const res = await client.get("/data", {
142
+ useCache: true,
143
+ cacheTtlMs: 30000,
144
+ cacheKey: "my-custom-key",
123
145
  });
124
- async function getUser(id) {
125
- const res = await client.request(`/users/${id}`);
126
- if (res.error) {
127
- console.error("Error:", res.error);
128
- } else {
129
- console.log("User:", res.data);
130
- }
131
- }
132
146
  ```
133
147
 
134
148
  ---
135
149
 
136
150
  #### RequestExecutor
137
151
 
138
- Wrapper for REST requests with retry and timeout support.
139
-
140
- #### Example
152
+ Wrapper for REST requests with retry, timeout (via AbortController), and backoff support.
141
153
 
142
154
  ```js
143
155
  import { RequestExecutor } from "rest-pipeline-js";
144
- const executor = new RequestExecutor({ baseURL: "https://api.example.com" });
145
- async function fetchData() {
146
- try {
147
- const res = await executor.execute("/data", { method: "GET" }, 3, 3000);
148
- if (res.error) {
149
- console.error("Error:", res.error);
150
- } else {
151
- console.log("Data:", res.data);
152
- }
153
- } catch (err) {
154
- console.error("Critical error:", err);
155
- }
156
- }
156
+
157
+ const executor = new RequestExecutor({
158
+ baseURL: "https://api.example.com",
159
+ retry: {
160
+ attempts: 3,
161
+ delayMs: 500,
162
+ backoffMultiplier: 2,
163
+ retriableStatus: [500, 502, 503],
164
+ },
165
+ });
166
+
167
+ // 5th arg: external AbortSignal (e.g. from orchestrator.abort())
168
+ const res = await executor.execute("/data", undefined, 3, 5000, signal);
157
169
  ```
158
170
 
171
+ Timeout is enforced via `AbortController` — the actual HTTP request is cancelled, not just the promise.
172
+
159
173
  ---
160
174
 
161
175
  #### PipelineOrchestrator
162
176
 
163
- Main class for building and managing a pipeline of sequential stages.
164
-
165
- ##### Key methods and parameters
166
-
167
- - **constructor(pipelineConfig, httpConfig, sharedData?, options?)**
168
- - `pipelineConfig` — array of stages, their params, conditions, handlers
169
- - `httpConfig` — HTTP client config
170
- - `sharedData` — shared data pool between stages
171
- - `options.autoReset` — whether to reset state after finish
172
- - **run(onStepPause?, externalSignal?)** — run the pipeline
173
- - `onStepPause(stepIndex, stepResult, stageResults)` — callback for pause/confirmation/modification between stages
174
- - `externalSignal` — external AbortSignal
175
- - Returns: `{ stageResults, success }`
176
- - **rerunStep(stepKey, options?)** — rerun a single stage
177
- - **subscribeProgress(listener)** — subscribe to progress updates
178
- - **subscribeStageResults(listener)** — subscribe to stage results
179
- - **subscribeStepProgress(stepKey, listener)** — subscribe to a specific stage's progress
180
- - **on(eventName, handler)** — universal event subscription
181
- - **onStepStart/Finish/Error(handler)** — subscribe to stage events
182
- - **getProgress()** — get current progress snapshot
183
- - **getProgressRef()** — get progress object (for reactivity)
184
- - **getLogs()** — get pipeline logs
185
- - **abort()** — abort pipeline
186
- - **isAborted()** — check if pipeline was aborted
187
-
188
- ##### Stage parameters
189
-
190
- - `key` — unique stage key
191
- - `command` — endpoint/command for request
192
- - `method` — HTTP method
193
- - `dependsOn` — array of stage keys this depends on
194
- - `condition({ prev, allResults, sharedData })` — condition function
195
- - `before({ prev, allResults, sharedData })` — pre-processing hook (called before request; can modify input)
196
- - `request({ prev, allResults, sharedData })` — custom request function. If before returns a value, it will be passed to request as prev.
197
- - `after({ result, allResults, sharedData })` — post-processing hook (called after request, before next stage; can modify result)
198
- - `pauseBefore` — optional pause (in ms) before request execution
199
- - `pauseAfter` — optional pause (in ms) after request execution
200
- - `retryCount`, `timeoutMs` — per-stage retry/timeout
201
- - `errorHandler({ error, key, sharedData })` — custom error handler
202
-
203
- ##### Step execution flow diagram
204
-
205
- ```
206
- ┌────────────┐
207
- │ before │
208
- │ (optional) │
209
- └─────┬──────┘
210
-
211
-
212
- ┌────────────┐
213
- │ request │
214
- └─────┬──────┘
215
-
216
-
217
- ┌────────────┐
218
- │ after │
219
- │ (optional) │
220
- └─────┬──────┘
221
-
222
-
223
- ┌────────────┐
224
- │ next step │
225
- └────────────┘
226
-
227
- If an error occurs at any stage:
228
- └─► errorHandler (if defined) → error result
229
- ```
230
-
231
- #### Example
177
+ Main class for building and managing a pipeline of sequential (and parallel) stages.
178
+
179
+ ##### Constructor
232
180
 
233
181
  ```js
234
- import { PipelineOrchestrator } from "rest-pipeline-js";
235
- const pipelineConfig = {
236
- stages: [
237
- { key: "first", command: "/api/first", method: "POST" },
238
- {
239
- key: "second",
240
- command: "/api/second",
241
- method: "POST",
242
- dependsOn: ["first"],
243
- },
244
- ],
245
- };
246
- const httpConfig = { baseURL: "https://api.example.com" };
247
- const sharedData = { sessionId: "abc" };
248
- const orchestrator = new PipelineOrchestrator(
249
- pipelineConfig,
250
- httpConfig,
251
- sharedData,
252
- );
253
- orchestrator.subscribeProgress((progress) => {
254
- console.log("Progress:", progress);
255
- });
256
- orchestrator.on("step:first:success", (payload) => {
257
- console.log("First stage done:", payload.data);
258
- });
259
- orchestrator
260
- .run(async (i, result) => {
261
- await new Promise((r) => setTimeout(r, 1000));
262
- return result;
263
- })
264
- .then((result) => console.log("Pipeline finished:", result))
265
- .catch((err) => console.error("Pipeline error:", err));
182
+ new PipelineOrchestrator({
183
+ config, // PipelineConfig stages and optional middleware
184
+ httpConfig?, // HttpConfig — HTTP client settings
185
+ sharedData?, // Record<string, any> shared pool across all stages
186
+ options?, // { autoReset?: boolean }
187
+ })
266
188
  ```
267
189
 
268
- ---
190
+ ##### Key methods
191
+
192
+ | Method | Description |
193
+ |--------|-------------|
194
+ | `run(onStepPause?, externalSignal?)` | Execute all stages. Returns `{ stageResults, success }` |
195
+ | `rerunStep(stepKey, options?)` | Re-execute a single stage (respects condition, before, after, middleware) |
196
+ | `abort()` | Abort pipeline execution (cancels the current HTTP request via AbortSignal) |
197
+ | `isAborted()` | Check if pipeline was aborted |
198
+ | `pause()` | Pause after the current stage completes |
199
+ | `resume()` | Resume a paused pipeline |
200
+ | `isPaused()` | Check if pipeline is paused |
201
+ | `exportState()` | Serialize stageResults and logs to a plain object |
202
+ | `importState(state)` | Restore stageResults and logs from a snapshot |
203
+ | `subscribeProgress(listener)` | Subscribe to progress updates |
204
+ | `subscribeStageResults(listener)` | Subscribe to stageResults changes |
205
+ | `subscribeStepProgress(stepKey, listener)` | Subscribe to a specific stage's progress |
206
+ | `on(eventName, handler)` | Subscribe to any event (`step:<key>:start\|success\|error\|skipped\|progress`, `log`) |
207
+ | `onStepStart/Finish/Error(handler)` | Subscribe to stage lifecycle events |
208
+ | `getProgress()` | Get current progress snapshot |
209
+ | `getLogs()` | Get all pipeline logs |
210
+ | `clearStageResults()` | Reset results and progress |
211
+
212
+ ##### Stage parameters (PipelineStageConfig)
213
+
214
+ | Parameter | Description |
215
+ |-----------|-------------|
216
+ | `key` | Unique stage identifier |
217
+ | `request({ prev, allResults, sharedData })` | Main stage function — return value becomes the stage result |
218
+ | `condition({ prev, allResults, sharedData })` | If returns `false`, stage is skipped with status `"skipped"` |
219
+ | `before({ prev, allResults, sharedData })` | Pre-processing hook — returned value replaces `prev` passed to `request` |
220
+ | `after({ result, allResults, sharedData })` | Post-processing hook — returned value replaces the stage result |
221
+ | `errorHandler({ error, key, sharedData })` | Per-stage error handler |
222
+ | `retryCount` | Override retry count for this stage |
223
+ | `timeoutMs` | Override timeout for this stage |
224
+ | `pauseBefore` | Delay in ms before executing `request` |
225
+ | `pauseAfter` | Delay in ms after executing `request` |
226
+
227
+ ##### Stage execution flow
228
+
229
+ ```
230
+ condition? → false → [status: skipped] → next stage
231
+ ↓ true
232
+ middleware.beforeEach
233
+
234
+ pauseBefore
235
+
236
+ before() hook
237
+
238
+ request()
239
+
240
+ after() hook
241
+
242
+ pauseAfter
243
+
244
+ middleware.afterEach
245
+
246
+ [status: success] → next stage
247
+
248
+ On error at any point:
249
+ └─► stage.errorHandler (if set) → middleware.onError → [status: error] → stop
250
+ ```
269
251
 
270
- #### ProgressTracker
252
+ ---
271
253
 
272
- Internal class for tracking pipeline progress.
254
+ ### Parallel stages
273
255
 
274
- #### Example
256
+ Group stages for concurrent execution using `parallel`:
275
257
 
276
258
  ```js
277
- import { ProgressTracker } from "rest-pipeline-js";
278
- const tracker = new ProgressTracker(3);
279
- tracker.subscribe((progress) => {
280
- console.log("Current progress:", progress);
259
+ const orchestrator = new PipelineOrchestrator({
260
+ config: {
261
+ stages: [
262
+ // Sequential stage
263
+ { key: "auth", request: async () => getToken() },
264
+
265
+ // Parallel group — all run concurrently
266
+ {
267
+ key: "load-data",
268
+ parallel: [
269
+ { key: "loadUsers", request: async () => fetchUsers() },
270
+ { key: "loadProducts", request: async () => fetchProducts() },
271
+ { key: "loadSettings", request: async () => fetchSettings() },
272
+ ],
273
+ },
274
+
275
+ // Sequential stage after the group
276
+ { key: "render", request: async ({ allResults }) => render(allResults) },
277
+ ],
278
+ },
281
279
  });
282
- tracker.updateStage(1, "success");
283
- console.log(tracker.getProgress());
284
280
  ```
285
281
 
282
+ - All stages in a `parallel` group run simultaneously via `Promise.all`.
283
+ - If **any** stage in the group fails, the pipeline stops and marks `success: false`.
284
+ - Each parallel stage has its own key and result in `stageResults`.
285
+ - `rerunStep(key)` works for stages inside parallel groups too.
286
+
286
287
  ---
287
288
 
288
- #### ErrorHandler
289
+ ### Global middleware
290
+
291
+ Apply hooks to every stage without modifying individual stage configs:
292
+
293
+ ```js
294
+ const orchestrator = new PipelineOrchestrator({
295
+ config: {
296
+ stages: [ /* ... */ ],
297
+ middleware: {
298
+ beforeEach: async ({ stage, index, sharedData }) => {
299
+ console.log(`[${index}] Starting: ${stage.key}`);
300
+ sharedData.startedAt = Date.now();
301
+ },
302
+ afterEach: async ({ stage, index, result, sharedData }) => {
303
+ const ms = Date.now() - sharedData.startedAt;
304
+ console.log(`[${index}] Done: ${stage.key} in ${ms}ms`, result.data);
305
+ },
306
+ onError: async ({ stage, error, sharedData }) => {
307
+ await reportError({ stage: stage.key, error, context: sharedData });
308
+ },
309
+ },
310
+ },
311
+ });
312
+ ```
313
+
314
+ Middleware runs in addition to (not instead of) per-stage `errorHandler`.
289
315
 
290
- Class for handling pipeline stage errors.
316
+ ---
317
+
318
+ ### Pause / Resume
291
319
 
292
- #### Example
320
+ Pause the pipeline after a stage and resume later:
293
321
 
294
322
  ```js
295
- import { ErrorHandler } from "rest-pipeline-js";
296
- const handler = new ErrorHandler();
297
- const error = handler.handle(new Error("fail"), "step1");
298
- console.log(error); // { type: 'unknown', error: [Error], stageKey: 'step1' }
323
+ const orchestrator = new PipelineOrchestrator({ config });
324
+
325
+ // Pause after step1 completes
326
+ orchestrator.on("step:step1:success", () => orchestrator.pause());
327
+
328
+ const runPromise = orchestrator.run();
329
+
330
+ // At some point later (e.g. after user confirmation):
331
+ await showConfirmDialog();
332
+ orchestrator.resume();
333
+
334
+ await runPromise;
299
335
  ```
300
336
 
301
- #### Types and interfaces
337
+ - `pause()` pipeline waits after the current stage finishes (including events).
338
+ - `resume()` — continues from the next stage.
339
+ - `abort()` while paused unblocks the pipeline and terminates it.
302
340
 
303
- - **HttpConfig** — REST client config (baseURL, timeout, headers, retry, cache, rateLimit, metrics)
304
- - **ApiError** — API error description
305
- - **ApiResponse<T>** API response (data, error, status)
306
- - **PipelineConfig, PipelineResult, PipelineStepEvent, PipelineStepStatus** — pipeline and stage types
341
+ ---
342
+
343
+ ### Export / Import state
344
+
345
+ Save and restore the pipeline state across page reloads or sessions:
346
+
347
+ ```js
348
+ const orchestrator = new PipelineOrchestrator({ config });
349
+ await orchestrator.run();
350
+
351
+ // Save state
352
+ const snapshot = orchestrator.exportState();
353
+ localStorage.setItem("pipelineState", JSON.stringify(snapshot));
354
+
355
+ // Later — restore and inspect without re-running
356
+ const saved = JSON.parse(localStorage.getItem("pipelineState"));
357
+ const orchestrator2 = new PipelineOrchestrator({ config });
358
+ orchestrator2.importState(saved);
359
+
360
+ console.log(orchestrator2.getProgress()); // restored progress
361
+ console.log(orchestrator2.getLogs()); // restored logs (timestamps as Date objects)
362
+ ```
363
+
364
+ `exportState()` returns `{ stageResults, logs }` — a plain JSON-serializable object. Timestamps in logs are stored as ISO strings and restored as `Date` objects on `importState`.
307
365
 
308
366
  ---
309
367
 
@@ -311,36 +369,38 @@ console.log(error); // { type: 'unknown', error: [Error], stageKey: 'step1' }
311
369
 
312
370
  #### Example: use in Vue component
313
371
 
314
- ```js
372
+ ```vue
315
373
  <script setup>
316
- import { ref } from 'vue';
317
- import { PipelineOrchestrator, usePipelineProgressVue, usePipelineRunVue } from 'rest-pipeline-js/vue';
318
- const pipelineConfig = { stages: [/* ... */] };
319
- const httpConfig = { baseURL: 'https://api.example.com' };
320
- const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
374
+ import { PipelineOrchestrator, usePipelineProgressVue, usePipelineRunVue } from "rest-pipeline-js/vue";
375
+
376
+ const orchestrator = new PipelineOrchestrator({ config: { stages: [/* ... */] } });
321
377
  const progress = usePipelineProgressVue(orchestrator);
322
- const { run, running, result, error } = usePipelineRunVue(orchestrator);
378
+ const { run, running, result, error, abort, pause, resume, rerunStep } = usePipelineRunVue(orchestrator);
323
379
  </script>
380
+
324
381
  <template>
325
382
  <div>
326
- <div>Current stage: {{ progress.value.currentStage }}</div>
383
+ <div>Current stage: {{ progress.currentStage }}</div>
327
384
  <button @click="run()" :disabled="running">Start</button>
385
+ <button @click="abort()" :disabled="!running">Abort</button>
386
+ <button @click="pause()">Pause</button>
387
+ <button @click="resume()">Resume</button>
328
388
  <div v-if="result">Done: {{ result }}</div>
329
389
  <div v-if="error">Error: {{ error.message }}</div>
330
390
  </div>
331
391
  </template>
332
392
  ```
333
393
 
334
- ---
335
-
336
- Composition functions for Vue 3 (import from `rest-pipeline-js/vue`):
394
+ Composition functions (import from `rest-pipeline-js/vue`):
337
395
 
338
- - **usePipelineProgressVue(orchestrator)** reactive pipeline progress (Ref<PipelineProgress>)
339
- - **usePipelineRunVue(orchestrator)** — run pipeline and get reactive status (run, running, result, error)
340
- - **usePipelineStepEventVue(orchestrator, stepKey, eventType)** subscribe to stage events (success, error, progress)
341
- - **usePipelineLogsVue(orchestrator)** reactive pipeline logs
342
- - **useRerunPipelineStepVue(orchestrator)** rerun a stage
343
- - **useRestClientVue(config)** reactive REST client (computed)
396
+ | Function | Returns | Description |
397
+ |----------|---------|-------------|
398
+ | `usePipelineProgressVue(orchestrator)` | `Ref<PipelineProgress>` | Reactive progress |
399
+ | `usePipelineRunVue(orchestrator)` | `{ run, running, result, error, stageResults, abort, pause, resume, rerunStep, clearStageResults }` | Run pipeline and get reactive state |
400
+ | `usePipelineStepEventVue(orchestrator, stepKey, eventType)` | `Ref<any>` | Last payload for a specific step event |
401
+ | `usePipelineLogsVue(orchestrator)` | `Ref<log[]>` | Reactive logs |
402
+ | `useRerunPipelineStepVue(orchestrator)` | `function` | Bound `rerunStep` |
403
+ | `useRestClientVue(config)` | `ComputedRef<RestClient>` | Reactive REST client |
344
404
 
345
405
  ---
346
406
 
@@ -349,28 +409,27 @@ Composition functions for Vue 3 (import from `rest-pipeline-js/vue`):
349
409
  #### Example: use in React component
350
410
 
351
411
  ```jsx
352
- import React from "react";
412
+ import { useRef } from "react";
353
413
  import {
354
414
  PipelineOrchestrator,
355
415
  usePipelineProgressReact,
356
416
  usePipelineRunReact,
357
417
  } from "rest-pipeline-js/react";
358
- const pipelineConfig = {
359
- stages: [
360
- /* ... */
361
- ],
362
- };
363
- const httpConfig = { baseURL: "https://api.example.com" };
364
- const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
418
+
419
+ const orchestrator = new PipelineOrchestrator({ config: { stages: [/* ... */] } });
420
+
365
421
  export function PipelineComponent() {
366
422
  const progress = usePipelineProgressReact(orchestrator);
367
- const [run, { running, result, error }] = usePipelineRunReact(orchestrator);
423
+ const [run, { running, result, error, abort, pause, resume, rerunStep }] =
424
+ usePipelineRunReact(orchestrator);
425
+
368
426
  return (
369
427
  <div>
370
428
  <div>Current stage: {progress.currentStage}</div>
371
- <button onClick={() => run()} disabled={running}>
372
- Start
373
- </button>
429
+ <button onClick={() => run()} disabled={running}>Start</button>
430
+ <button onClick={() => abort()} disabled={!running}>Abort</button>
431
+ <button onClick={() => pause()}>Pause</button>
432
+ <button onClick={() => resume()}>Resume</button>
374
433
  {result && <div>Done: {JSON.stringify(result)}</div>}
375
434
  {error && <div>Error: {error.message}</div>}
376
435
  </div>
@@ -378,69 +437,56 @@ export function PipelineComponent() {
378
437
  }
379
438
  ```
380
439
 
381
- ---
382
-
383
- Hooks for React (import from `rest-pipeline-js/react`):
384
-
385
- - **usePipelineProgressReact(orchestrator)** — subscribe to pipeline progress (PipelineProgress)
386
- - **usePipelineRunReact(orchestrator)** — run pipeline and get status ([run, { running, result, error }])
387
- - **usePipelineStepEventReact(orchestrator, stepKey, eventType)** — subscribe to stage events (success/error/progress)
388
- - **usePipelineLogsReact(orchestrator)** — subscribe to pipeline logs
389
- - **useRerunPipelineStepReact(orchestrator)** — rerun a stage
390
- - **useRestClientReact(config)** — memoized REST client
391
-
392
- ---
393
-
394
- ## Requirements
440
+ Hooks (import from `rest-pipeline-js/react`):
395
441
 
396
- - Node.js >= 14.0.0
397
- - Modern browser with ES2020 support
442
+ | Hook | Returns | Description |
443
+ |------|---------|-------------|
444
+ | `usePipelineProgressReact(orchestrator)` | `PipelineProgress` | Reactive progress |
445
+ | `usePipelineRunReact(orchestrator)` | `[run, { running, result, error, stageResults, abort, pause, resume, rerunStep }]` | Run pipeline and get state |
446
+ | `usePipelineStepEventReact(orchestrator, stepKey, eventType)` | `any` | Last payload for a specific step event |
447
+ | `usePipelineLogsReact(orchestrator)` | `log[]` | Reactive logs |
448
+ | `useRerunPipelineStepReact(orchestrator)` | `function` | Bound `rerunStep` |
449
+ | `useRestClientReact(config)` | `RestClient` | Memoized REST client |
398
450
 
399
451
  ---
400
452
 
401
- ## Entry points and imports
402
-
403
- The package has three entry points so that bundlers do not pull in React when you only use core or Vue.
453
+ ## Entry points
404
454
 
405
455
  | Entry point | Use for | Contents |
406
456
  |-------------|---------|----------|
407
- | `rest-pipeline-js` | Core only | `PipelineOrchestrator`, `createRestClient`, types, `rest-client`, `request-executor`, `error-handler`, `progress-tracker`. No Vue/React. |
408
- | `rest-pipeline-js/vue` | Vue projects | Everything from core + Vue hooks: `usePipelineProgressVue`, `usePipelineRunVue`, `useRestClientVue`, `usePipelineStepEventVue`, `usePipelineLogsVue`, `useRerunPipelineStepVue`. |
409
- | `rest-pipeline-js/react` | React projects | Everything from core + React hooks: `usePipelineProgressReact`, `usePipelineRunReact`, `useRestClientReact`, `usePipelineStepEventReact`, `usePipelineLogsReact`, `useRerunPipelineStepReact`. |
457
+ | `rest-pipeline-js` | Core only | `PipelineOrchestrator`, `createRestClient`, types, utilities. No Vue/React. |
458
+ | `rest-pipeline-js/vue` | Vue projects | Core + Vue composition functions |
459
+ | `rest-pipeline-js/react` | React projects | Core + React hooks |
410
460
 
411
- **Recommended imports:**
412
-
413
- - **Core only** (no framework):
461
+ ```js
462
+ // Core only
463
+ import { createRestClient, PipelineOrchestrator } from "rest-pipeline-js";
414
464
 
415
- ```js
416
- import { createRestClient, PipelineOrchestrator } from "rest-pipeline-js";
417
- ```
465
+ // Vue
466
+ import { PipelineOrchestrator, usePipelineRunVue } from "rest-pipeline-js/vue";
418
467
 
419
- - **Vue** (core + Vue hooks):
468
+ // React
469
+ import { PipelineOrchestrator, usePipelineRunReact } from "rest-pipeline-js/react";
470
+ ```
420
471
 
421
- ```js
422
- import { PipelineOrchestrator, usePipelineRunVue } from "rest-pipeline-js/vue";
423
- ```
472
+ `sideEffects: false` — unused entry points are tree-shaken. `react`/`react-dom` are `peerDependencies`.
424
473
 
425
- - **React** (core + React hooks):
474
+ ---
426
475
 
427
- ```js
428
- import { PipelineOrchestrator, usePipelineRunReact } from "rest-pipeline-js/react";
429
- ```
476
+ ## Requirements
430
477
 
431
- If you use only `rest-pipeline-js` or `rest-pipeline-js/vue`, the bundler will not resolve or include `react`. The package sets `sideEffects: false` and declares `react`/`react-dom` as `peerDependencies` for the React entry point.
478
+ - Node.js >= 14.0.0
479
+ - Modern browser with ES2019+ support
432
480
 
433
- If you want, I can prepare a release (bump version and build) with these changes.
481
+ ---
434
482
 
435
- ## Development & Contribution
483
+ ## Development
436
484
 
437
485
  ```bash
438
- # Clone repository
439
486
  git clone https://github.com/macrulezru/pipeline-js.git
440
487
  cd pipeline-js
441
488
  npm install
442
489
  npm test
443
- npm run lint
444
490
  ```
445
491
 
446
492
  ---
@@ -455,15 +501,12 @@ MIT
455
501
 
456
502
  Danil Lisin Vladimirovich aka Macrulez
457
503
 
458
- GitHub: [macrulezru](https://github.com/macrulezru)
504
+ GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru](https://macrulez.ru/)
459
505
 
460
- Website: [macrulez.ru](https://macrulez.ru/)
506
+ Questions and bugs — [issues](https://github.com/macrulezru/pipeline-js/issues)
461
507
 
462
508
  ---
463
-
464
- ## Support
465
-
466
- Questions and bugs — via [issue](https://github.com/macrulezru/pipeline-js/issues)
509
+ ---
467
510
 
468
511
  ## Установка
469
512
 
@@ -475,7 +518,7 @@ npm i rest-pipeline-js
475
518
 
476
519
  ### Базовый модуль (rest-pipeline-js)
477
520
 
478
- #### Пример: создание REST клиента и выполнение запроса
521
+ #### Пример: создание REST клиента и выполнение запросов
479
522
 
480
523
  ```js
481
524
  import { createRestClient } from "rest-pipeline-js";
@@ -484,404 +527,409 @@ const client = createRestClient({
484
527
  baseURL: "https://api.example.com",
485
528
  timeout: 5000,
486
529
  headers: { Authorization: "Bearer TOKEN" },
530
+ retry: { attempts: 2, delayMs: 500, backoffMultiplier: 2, retriableStatus: [429, 500, 503] },
531
+ cache: { enabled: true, ttlMs: 60000 },
532
+ rateLimit: { maxConcurrent: 3, maxRequestsPerInterval: 10, intervalMs: 1000 },
487
533
  });
488
534
 
489
- async function fetchUser(id) {
490
- const res = await client.request(`/users/${id}`);
491
- if (res.error) {
492
- console.error(res.error);
493
- } else {
494
- console.log(res.data);
495
- }
496
- }
535
+ const res = await client.get("/users/1");
536
+ console.log(res.data);
537
+
538
+ // PATCH-запрос
539
+ await client.patch("/users/1", { name: "Alice" });
540
+
541
+ // Отменяемый запрос
542
+ client.cancellableRequest("my-key", "/search", { params: { q: "foo" } });
543
+ client.cancelRequest("my-key"); // отмена
497
544
  ```
498
545
 
499
- #### Пример: запуск pipeline, обработка ошибок, отслеживание выполнения и использование общего пула данных
546
+ ---
547
+
548
+ #### Пример: запуск pipeline, middleware, паузы, общий пул данных
500
549
 
501
550
  ```js
502
551
  import { PipelineOrchestrator } from "rest-pipeline-js";
503
552
 
504
- const pipelineConfig = {
505
- stages: [
506
- {
507
- key: "step1",
508
- command: "/api/step1",
509
- method: "POST",
510
- // Можно добавить кастомные параметры шага
511
- },
512
- {
513
- key: "step2",
514
- command: "/api/step2",
515
- method: "POST",
516
- dependsOn: ["step1"], // step2 выполнится только после step1
553
+ const orchestrator = new PipelineOrchestrator({
554
+ config: {
555
+ stages: [
556
+ {
557
+ key: "fetchUser",
558
+ request: async ({ sharedData }) => {
559
+ const res = await fetch(`/api/users/${sharedData.userId}`);
560
+ return res.json();
561
+ },
562
+ },
563
+ {
564
+ key: "processData",
565
+ condition: ({ prev }) => prev !== null,
566
+ before: ({ prev }) => ({ ...prev, processed: true }),
567
+ request: async ({ prev }) => prev,
568
+ after: ({ result }) => ({ ...result, finishedAt: Date.now() }),
569
+ },
570
+ ],
571
+ middleware: {
572
+ beforeEach: ({ stage }) => console.log("Старт:", stage.key),
573
+ afterEach: ({ stage, result }) => console.log("Готово:", stage.key, result.data),
574
+ onError: ({ stage, error }) => console.error("Ошибка в", stage.key, error),
517
575
  },
518
- ],
519
- };
520
-
521
- const httpConfig = {
522
- baseURL: "https://api.example.com",
523
- timeout: 7000,
524
- headers: { Authorization: "Bearer TOKEN" },
525
- retry: { attempts: 2, delayMs: 1000 },
526
- cache: { enabled: true, ttlMs: 60000 },
527
- rateLimit: { maxConcurrent: 2 },
528
- metrics: {
529
- onRequestStart: (info) => console.log("Start:", info),
530
- onRequestEnd: (info) => console.log("End:", info),
531
576
  },
532
- };
533
-
534
- // Общий пул данных между шагами
535
- const sharedData = { sessionId: "abc123" };
536
-
537
- const orchestrator = new PipelineOrchestrator(
538
- pipelineConfig,
539
- httpConfig,
540
- sharedData,
541
- { autoReset: true },
542
- );
543
-
544
- // Отслеживание прогресса
545
- orchestrator.subscribeProgress((progress) => {
546
- console.log(
547
- "Текущий шаг:",
548
- progress.currentStage,
549
- "Статусы:",
550
- progress.stageStatuses,
551
- );
552
- });
553
-
554
- // Подписка на события успеха/ошибки шага
555
- orchestrator.on("step:step1:success", (payload) => {
556
- console.log("Step 1 завершён успешно:", payload.data);
557
- });
558
- orchestrator.on("step:step2:error", (payload) => {
559
- console.error("Ошибка на step2:", payload.error);
577
+ httpConfig: {
578
+ baseURL: "https://api.example.com",
579
+ retry: { attempts: 2, delayMs: 1000, backoffMultiplier: 2 },
580
+ cache: { enabled: true, ttlMs: 60000 },
581
+ },
582
+ sharedData: { userId: 42 },
583
+ options: { autoReset: true },
560
584
  });
561
585
 
562
- // Подписка на все логи pipeline
563
- orchestrator.on("log", () => {
564
- console.log("Логи:", orchestrator.getLogs());
586
+ orchestrator.subscribeProgress((progress) => {
587
+ console.log("Шаг:", progress.currentStage, "Статусы:", progress.stageStatuses);
565
588
  });
566
589
 
567
- // Запуск pipeline с передачей параметров
568
- orchestrator
569
- .run({ foo: "bar" })
570
- .then((result) => {
571
- console.log("Pipeline завершён. Итог:", result);
572
- // Доступ к результатам всех шагов:
573
- console.log("Результаты шагов:", result.stageResults);
574
- })
575
- .catch((err) => {
576
- // Глобальная обработка ошибок pipeline
577
- console.error("Pipeline error:", err);
578
- });
579
-
580
- // Повторный запуск шага (например, после ошибки)
581
- // orchestrator.rerunStep('step2');
590
+ const result = await orchestrator.run();
591
+ console.log("Pipeline завершён:", result.success);
582
592
  ```
583
593
 
584
594
  ---
585
595
 
586
- ---
596
+ ### Основные классы и функции
587
597
 
588
- Модуль предоставляет универсальный механизм для построения и управления REST API pipeline с поддержкой прогресса, обработки ошибок, подписки на события и расширяемости.
598
+ #### createRestClient(config: HttpConfig): RestClient
589
599
 
590
- #### Основные классы и функции
600
+ Создаёт REST-клиент с поддержкой кэширования, rate limiting, retry и метрик.
601
+
602
+ **Методы клиента:**
603
+
604
+ | Метод | Описание |
605
+ |-------|----------|
606
+ | `get(url, config?)` | GET-запрос |
607
+ | `post(url, data?, config?)` | POST-запрос |
608
+ | `put(url, data?, config?)` | PUT-запрос |
609
+ | `patch(url, data?, config?)` | PATCH-запрос |
610
+ | `delete(url, config?)` | DELETE-запрос |
611
+ | `request(url, config?)` | Произвольный запрос |
612
+ | `cancellableRequest(key, url, config?)` | Запрос, отменяемый по ключу |
613
+ | `cancelRequest(key)` | Отменить запрос по ключу |
614
+ | `clearCache()` | Очистить кэш ответов этого клиента |
615
+
616
+ **Параметры HttpConfig:**
617
+
618
+ | Параметр | Описание |
619
+ |----------|----------|
620
+ | `baseURL` | Базовый URL для всех запросов |
621
+ | `timeout` | Таймаут запроса в мс |
622
+ | `headers` | Заголовки по умолчанию |
623
+ | `withCredentials` | Включить cookies |
624
+ | `retry.attempts` | Количество повторных попыток |
625
+ | `retry.delayMs` | Базовая задержка между попытками в мс |
626
+ | `retry.backoffMultiplier` | Множитель экспоненциального backoff |
627
+ | `retry.retriableStatus` | HTTP-статусы для повтора (например, `[429, 500, 503]`) |
628
+ | `cache.enabled` | Кэшировать GET-ответы |
629
+ | `cache.ttlMs` | Время жизни кэша в мс |
630
+ | `rateLimit.maxConcurrent` | Максимум одновременных запросов |
631
+ | `rateLimit.maxRequestsPerInterval` | Максимум запросов за окно времени |
632
+ | `rateLimit.intervalMs` | Размер временного окна в мс |
633
+ | `metrics.onRequestStart` | Callback при старте запроса |
634
+ | `metrics.onRequestEnd` | Callback при завершении (включает duration и bytes) |
635
+
636
+ **Переопределение кэша на уровне запроса:**
591
637
 
592
- ---
638
+ ```js
639
+ const res = await client.get("/data", {
640
+ useCache: true,
641
+ cacheTtlMs: 30000,
642
+ cacheKey: "my-key",
643
+ });
644
+ ```
593
645
 
594
- ### createRestClient(config: HttpConfig): RestClient
646
+ ---
595
647
 
596
- Создаёт REST-клиент с поддержкой расширенных возможностей для работы с HTTP API.
648
+ #### RequestExecutor
597
649
 
598
- #### Пример
650
+ Обёртка для выполнения запросов с поддержкой retry, таймаута и AbortSignal.
599
651
 
600
652
  ```js
601
- import { createRestClient } from "rest-pipeline-js";
653
+ import { RequestExecutor } from "rest-pipeline-js";
602
654
 
603
- const client = createRestClient({
655
+ const executor = new RequestExecutor({
604
656
  baseURL: "https://api.example.com",
605
- timeout: 5000,
606
- headers: { Authorization: "Bearer TOKEN" },
607
- retry: { attempts: 2 },
608
- cache: { enabled: true, ttlMs: 60000 },
657
+ retry: {
658
+ attempts: 3,
659
+ delayMs: 500,
660
+ backoffMultiplier: 2,
661
+ retriableStatus: [500, 502, 503],
662
+ },
609
663
  });
610
664
 
611
- async function getUser(id) {
612
- const res = await client.request(`/users/${id}`);
613
- if (res.error) {
614
- console.error("Ошибка:", res.error);
615
- } else {
616
- console.log("Пользователь:", res.data);
617
- }
618
- }
665
+ // 5-й аргумент — внешний AbortSignal (например от orchestrator.abort())
666
+ const res = await executor.execute("/data", undefined, 3, 5000, signal);
619
667
  ```
620
668
 
669
+ Таймаут реализован через `AbortController` — HTTP-запрос реально отменяется, а не просто отклоняется промис.
670
+
621
671
  ---
622
672
 
623
- ### RequestExecutor
673
+ #### PipelineOrchestrator
624
674
 
625
- Обёртка для выполнения REST-запросов с поддержкой автоматического retry и таймаута.
675
+ Основной класс для управления конвейером последовательных и параллельных шагов.
626
676
 
627
- #### Пример
677
+ ##### Конструктор
628
678
 
629
679
  ```js
630
- import { RequestExecutor } from "rest-pipeline-js";
631
-
632
- const executor = new RequestExecutor({ baseURL: "https://api.example.com" });
633
-
634
- async function fetchData() {
635
- try {
636
- const res = await executor.execute("/data", { method: "GET" }, 3, 3000);
637
- if (res.error) {
638
- console.error("Ошибка:", res.error);
639
- } else {
640
- console.log("Данные:", res.data);
641
- }
642
- } catch (err) {
643
- console.error("Критическая ошибка:", err);
644
- }
645
- }
680
+ new PipelineOrchestrator({
681
+ config, // PipelineConfig — шаги и опциональный middleware
682
+ httpConfig?, // HttpConfig настройки HTTP-клиента
683
+ sharedData?, // Record<string, any> — общий пул данных для всех шагов
684
+ options?, // { autoReset?: boolean }
685
+ })
646
686
  ```
647
687
 
648
- ---
688
+ ##### Основные методы
689
+
690
+ | Метод | Описание |
691
+ |-------|----------|
692
+ | `run(onStepPause?, externalSignal?)` | Запустить все шаги. Возвращает `{ stageResults, success }` |
693
+ | `rerunStep(stepKey, options?)` | Повторно выполнить один шаг (учитывает condition, before, after, middleware) |
694
+ | `abort()` | Отменить выполнение (реально отменяет текущий HTTP-запрос через AbortSignal) |
695
+ | `isAborted()` | Проверить, был ли pipeline отменён |
696
+ | `pause()` | Приостановить после завершения текущего шага |
697
+ | `resume()` | Продолжить выполнение после паузы |
698
+ | `isPaused()` | Проверить, приостановлен ли pipeline |
699
+ | `exportState()` | Экспортировать stageResults и логи в простой объект |
700
+ | `importState(state)` | Восстановить состояние из снимка |
701
+ | `subscribeProgress(listener)` | Подписаться на изменения прогресса |
702
+ | `subscribeStageResults(listener)` | Подписаться на изменения результатов шагов |
703
+ | `subscribeStepProgress(stepKey, listener)` | Подписаться на прогресс конкретного шага |
704
+ | `on(eventName, handler)` | Подписаться на события (`step:<key>:start\|success\|error\|skipped\|progress`, `log`) |
705
+ | `onStepStart/Finish/Error(handler)` | Подписка на жизненный цикл шага |
706
+ | `getProgress()` | Получить снимок текущего прогресса |
707
+ | `getLogs()` | Получить все логи pipeline |
708
+ | `clearStageResults()` | Сбросить результаты и прогресс |
709
+
710
+ ##### Параметры шага (PipelineStageConfig)
711
+
712
+ | Параметр | Описание |
713
+ |----------|----------|
714
+ | `key` | Уникальный идентификатор шага |
715
+ | `request({ prev, allResults, sharedData })` | Основная функция шага — возвращаемое значение становится результатом |
716
+ | `condition({ prev, allResults, sharedData })` | Если возвращает `false` — шаг пропускается со статусом `"skipped"` |
717
+ | `before({ prev, allResults, sharedData })` | Pre-processing хук — возвращаемое значение заменяет `prev` в `request` |
718
+ | `after({ result, allResults, sharedData })` | Post-processing хук — возвращаемое значение заменяет результат шага |
719
+ | `errorHandler({ error, key, sharedData })` | Обработчик ошибок шага |
720
+ | `retryCount` | Переопределение количества retry для этого шага |
721
+ | `timeoutMs` | Переопределение таймаута для этого шага |
722
+ | `pauseBefore` | Пауза в мс перед выполнением `request` |
723
+ | `pauseAfter` | Пауза в мс после выполнения `request` |
649
724
 
650
- ### PipelineOrchestrator
725
+ ##### Диаграмма выполнения шага
651
726
 
652
- Основной класс для построения и управления конвейером (pipeline) из последовательных шагов.
727
+ ```
728
+ condition? → false → [статус: skipped] → следующий шаг
729
+ ↓ true
730
+ middleware.beforeEach
731
+
732
+ pauseBefore
733
+
734
+ before() хук
735
+
736
+ request()
737
+
738
+ after() хук
739
+
740
+ pauseAfter
741
+
742
+ middleware.afterEach
743
+
744
+ [статус: success] → следующий шаг
745
+
746
+ При ошибке на любом этапе:
747
+ └─► stage.errorHandler (если задан) → middleware.onError → [статус: error] → стоп
748
+ ```
653
749
 
654
- #### Основные методы и параметры
750
+ ---
655
751
 
656
- - **constructor(pipelineConfig, httpConfig, sharedData?, options?)** — создание экземпляра:
657
- - `pipelineConfig` — массив шагов (stages), их параметры, условия, обработчики
658
- - `httpConfig` — настройки HTTP клиента
659
- - `sharedData` — общий пул данных между шагами
660
- - `options.autoReset` — сбрасывать ли состояние после завершения
752
+ ### Параллельные шаги
661
753
 
662
- - **run(onStepPause?, externalSignal?)** запуск конвейера
663
- - `onStepPause(stepIndex, stepResult, stageResults)` — callback для паузы/подтверждения/модификации результата между шагами (можно реализовать задержку, диалог, логику)
664
- - `externalSignal` — внешний AbortSignal для отмены
665
- - Возвращает: `{ stageResults, success }`
754
+ Группируйте шаги для параллельного выполнения с помощью `parallel`:
666
755
 
667
- - **rerunStep(stepKey, options?)** — повторно выполнить один шаг
668
- - `onStepPause` и `externalSignal` аналогично run
669
- - Возвращает результат шага
756
+ ```js
757
+ const orchestrator = new PipelineOrchestrator({
758
+ config: {
759
+ stages: [
760
+ // Обычный последовательный шаг
761
+ { key: "auth", request: async () => getToken() },
762
+
763
+ // Параллельная группа — все шаги выполняются одновременно
764
+ {
765
+ key: "load-data",
766
+ parallel: [
767
+ { key: "loadUsers", request: async () => fetchUsers() },
768
+ { key: "loadProducts", request: async () => fetchProducts() },
769
+ { key: "loadSettings", request: async () => fetchSettings() },
770
+ ],
771
+ },
772
+
773
+ // Последовательный шаг после группы
774
+ {
775
+ key: "render",
776
+ request: async ({ allResults }) => render(allResults),
777
+ },
778
+ ],
779
+ },
780
+ });
670
781
 
671
- - **subscribeProgress(listener)** подписка на прогресс выполнения (listener получает PipelineProgress)
672
- - **subscribeStageResults(listener)** подписка на изменения результатов всех шагов
673
- - **subscribeStepProgress(stepKey, listener)** — подписка на прогресс конкретного шага
674
- - **on(eventName, handler)** — универсальная подписка на события:
675
- - `step:<stepKey>:start|success|error|progress` — события по шагам
676
- - `log` — новые логи
677
- - любые кастомные события
678
- - **onStepStart/Finish/Error(handler)** — подписка на начало/успех/ошибку шага (PipelineStepEvent)
679
- - **getProgress()** — получить текущий прогресс (snapshot)
680
- - **getProgressRef()** — получить ссылку на объект прогресса (для реактивности)
681
- - **getLogs()** — получить массив логов pipeline
682
- - **abort()** — отменить выполнение пайплайна
683
- - **isAborted()** — проверить, был ли пайплайн отменён
782
+ const result = await orchestrator.run();
783
+ console.log(result.stageResults.loadUsers.data); // результат параллельного шага
784
+ console.log(result.stageResults.loadProducts.data);
785
+ ```
684
786
 
685
- #### Важные параметры шага (stage):
787
+ - Все шаги группы выполняются одновременно через `Promise.all`.
788
+ - Если **хотя бы один** шаг группы завершился с ошибкой — pipeline останавливается, `success: false`.
789
+ - Каждый параллельный шаг имеет собственный ключ и запись в `stageResults`.
790
+ - `rerunStep(key)` работает в том числе для шагов внутри параллельных групп.
686
791
 
687
- - `key` — уникальный ключ шага
688
- - `command` — команда/endpoint для запроса
689
- - `method` — HTTP-метод
690
- - `dependsOn` — массив ключей шагов, от которых зависит этот шаг
691
- - `condition({ prev, allResults, sharedData })` — функция-условие для выполнения шага
692
- - `before({ prev, allResults, sharedData })` — before-хук (вызывается перед запросом; может изменить входные данные)
693
- - `request({ prev, allResults, sharedData })` — кастомная функция запроса (альтернатива command). Если before возвращает значение, оно будет передано в request как prev.
694
- - `after({ result, allResults, sharedData })` — post-processing хук (вызывается после запроса, до перехода к следующему этапу; может модифицировать результат)
695
- - `pauseBefore` — опциональная пауза (в миллисекундах) перед выполнением запроса
696
- - `pauseAfter` — опциональная пауза (в миллисекундах) после выполнения запроса
697
- - `retryCount`, `timeoutMs` — индивидуальные настройки повтора и таймаута
698
- - `errorHandler({ error, key, sharedData })` — обработчик ошибок шага
792
+ ---
699
793
 
700
- ##### Диаграмма выполнения шага
794
+ ### Global middleware
701
795
 
702
- ```
703
- ┌───────────────┐
704
- │ before │
705
- │ (опционально) │
706
- └─────┬─────────┘
707
-
708
-
709
- ┌────────────┐
710
- │ request │
711
- └─────┬──────┘
712
-
713
-
714
- ┌───────────────┐
715
- │ after │
716
- │ (опционально) │
717
- └─────┬─────────┘
718
-
719
-
720
- ┌────────────┐
721
- │ следующий │
722
- │ шаг │
723
- └────────────┘
724
-
725
- Если возникает ошибка на любом этапе:
726
- └─► errorHandler (если определён) → результат с ошибкой
727
- ```
728
-
729
- #### Пример
796
+ Применяйте хуки ко всем шагам без изменения их конфигураций:
730
797
 
731
798
  ```js
732
- import { PipelineOrchestrator } from "rest-pipeline-js";
733
-
734
- const pipelineConfig = {
735
- stages: [
736
- { key: "first", command: "/api/first", method: "POST" },
737
- {
738
- key: "second",
739
- command: "/api/second",
740
- method: "POST",
741
- dependsOn: ["first"],
799
+ const orchestrator = new PipelineOrchestrator({
800
+ config: {
801
+ stages: [ /* ... */ ],
802
+ middleware: {
803
+ beforeEach: async ({ stage, index, sharedData }) => {
804
+ console.log(`[${index}] Старт: ${stage.key}`);
805
+ sharedData.startedAt = Date.now();
806
+ },
807
+ afterEach: async ({ stage, index, result, sharedData }) => {
808
+ const ms = Date.now() - sharedData.startedAt;
809
+ console.log(`[${index}] Готово: ${stage.key} за ${ms}мс`, result.data);
810
+ },
811
+ onError: async ({ stage, error, sharedData }) => {
812
+ await reportError({ stage: stage.key, error, context: sharedData });
813
+ },
742
814
  },
743
- ],
744
- };
745
- const httpConfig = { baseURL: "https://api.example.com" };
746
- const sharedData = { sessionId: "abc" };
747
- const orchestrator = new PipelineOrchestrator(
748
- pipelineConfig,
749
- httpConfig,
750
- sharedData,
751
- );
752
-
753
- orchestrator.subscribeProgress((progress) => {
754
- console.log("Прогресс:", progress);
755
- });
756
-
757
- orchestrator.on("step:first:success", (payload) => {
758
- console.log("Первый шаг выполнен:", payload.data);
815
+ },
759
816
  });
760
-
761
- // Пауза 1 секунда между шагами
762
- orchestrator
763
- .run(async (i, result) => {
764
- await new Promise((r) => setTimeout(r, 1000));
765
- return result;
766
- })
767
- .then((result) => console.log("Pipeline завершён:", result))
768
- .catch((err) => console.error("Ошибка pipeline:", err));
769
817
  ```
770
818
 
771
- ---
819
+ Middleware вызывается дополнительно к per-stage `errorHandler`, а не вместо него.
772
820
 
773
- ### ProgressTracker
821
+ ---
774
822
 
775
- Внутренний класс для отслеживания прогресса pipeline.
823
+ ### Пауза и возобновление
776
824
 
777
- #### Пример
825
+ Останавливайте pipeline после шага и продолжайте по команде:
778
826
 
779
827
  ```js
780
- import { ProgressTracker } from "rest-pipeline-js";
828
+ const orchestrator = new PipelineOrchestrator({ config });
781
829
 
782
- const tracker = new ProgressTracker(3); // 3 шага
783
- tracker.subscribe((progress) => {
784
- console.log("Текущий прогресс:", progress);
785
- });
786
- tracker.updateStage(1, "success");
787
- console.log(tracker.getProgress());
830
+ // Пауза после завершения step1
831
+ orchestrator.on("step:step1:success", () => orchestrator.pause());
832
+
833
+ const runPromise = orchestrator.run();
834
+
835
+ // В любой момент позже (например, после подтверждения пользователем):
836
+ await showConfirmDialog();
837
+ orchestrator.resume();
838
+
839
+ await runPromise;
788
840
  ```
789
841
 
790
- ---
842
+ - `pause()` — pipeline ждёт после завершения текущего шага (включая все события).
843
+ - `resume()` — продолжает выполнение со следующего шага.
844
+ - `abort()` во время паузы — разблокирует pipeline и завершает его.
791
845
 
792
- ### ErrorHandler
846
+ ---
793
847
 
794
- Класс для обработки ошибок шагов pipeline.
848
+ ### Экспорт и восстановление состояния
795
849
 
796
- #### Пример
850
+ Сохраняйте и восстанавливайте состояние pipeline между перезагрузками или сессиями:
797
851
 
798
852
  ```js
799
- import { ErrorHandler } from "rest-pipeline-js";
853
+ const orchestrator = new PipelineOrchestrator({ config });
854
+ await orchestrator.run();
800
855
 
801
- const handler = new ErrorHandler();
802
- const error = handler.handle(new Error("fail"), "step1");
803
- console.log(error); // { type: 'unknown', error: [Error], stageKey: 'step1' }
804
- ```
856
+ // Сохраняем снимок состояния
857
+ const snapshot = orchestrator.exportState();
858
+ localStorage.setItem("pipelineState", JSON.stringify(snapshot));
805
859
 
806
- #### Типы и интерфейсы:
860
+ // Позже восстанавливаем
861
+ const saved = JSON.parse(localStorage.getItem("pipelineState"));
862
+ const orchestrator2 = new PipelineOrchestrator({ config });
863
+ orchestrator2.importState(saved);
807
864
 
808
- - **HttpConfig** — конфигурация REST клиента (baseURL, timeout, headers, retry, cache, rateLimit, metrics)
809
- - **ApiError**описание ошибки API
810
- - **ApiResponse<T>** — ответ API (данные, ошибка, статус)
811
- - **PipelineConfig, PipelineResult, PipelineStepEvent, PipelineStepStatus** — описание pipeline и стадий
865
+ console.log(orchestrator2.getProgress()); // восстановленный прогресс
866
+ console.log(orchestrator2.getLogs()); // восстановленные логи (timestamps объекты Date)
867
+ ```
812
868
 
813
- ---
869
+ `exportState()` возвращает `{ stageResults, logs }` — обычный JSON-сериализуемый объект. Временны́е метки в логах хранятся как ISO-строки и восстанавливаются как объекты `Date` при `importState`.
814
870
 
815
- ### Расширение для Vue
871
+ ---
816
872
 
817
- #### Пример: использование во Vue компоненте
873
+ ### Интеграция с Vue
818
874
 
819
- ```js
875
+ ```vue
820
876
  <script setup>
821
- import { ref } from 'vue';
822
- import { PipelineOrchestrator, usePipelineProgressVue, usePipelineRunVue } from 'rest-pipeline-js/vue';
823
-
824
- const pipelineConfig = { stages: [/* ... */] };
825
- const httpConfig = { baseURL: 'https://api.example.com' };
826
- const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
877
+ import { PipelineOrchestrator, usePipelineProgressVue, usePipelineRunVue } from "rest-pipeline-js/vue";
827
878
 
879
+ const orchestrator = new PipelineOrchestrator({ config: { stages: [/* ... */] } });
828
880
  const progress = usePipelineProgressVue(orchestrator);
829
- const { run, running, result, error } = usePipelineRunVue(orchestrator);
881
+ const { run, running, result, error, abort, pause, resume, rerunStep } = usePipelineRunVue(orchestrator);
830
882
  </script>
831
883
 
832
884
  <template>
833
- <div>
834
- <div>Текущий шаг: {{ progress.value.currentStage }}</div>
835
- <button @click="run()" :disabled="running">Старт</button>
836
- <div v-if="result">Готово: {{ result }}</div>
837
- <div v-if="error">Ошибка: {{ error.message }}</div>
838
- </div>
885
+ <div>
886
+ <div>Текущий шаг: {{ progress.currentStage }}</div>
887
+ <button @click="run()" :disabled="running">Старт</button>
888
+ <button @click="abort()" :disabled="!running">Отмена</button>
889
+ <button @click="pause()">Пауза</button>
890
+ <button @click="resume()">Продолжить</button>
891
+ <div v-if="result">Готово: {{ result }}</div>
892
+ <div v-if="error">Ошибка: {{ error.message }}</div>
893
+ </div>
839
894
  </template>
840
895
  ```
841
896
 
842
- ---
843
-
844
- Экспортируются composition-функции для интеграции rest-pipeline-js с Vue 3 (импортировать из `rest-pipeline-js/vue`):
897
+ Composition-функции (импорт из `rest-pipeline-js/vue`):
845
898
 
846
- - **usePipelineProgressVue(orchestrator)** реактивный прогресс pipeline (Ref<PipelineProgress>)
847
- - **usePipelineRunVue(orchestrator)** — запуск pipeline и реактивные статусы (run, running, result, error)
848
- - **usePipelineStepEventVue(orchestrator, stepKey, eventType)** подписка на события шага (успех, ошибка, прогресс)
849
- - **usePipelineLogsVue(orchestrator)** реактивные логи pipeline
850
- - **useRerunPipelineStepVue(orchestrator)** функция для повторного запуска шага
851
- - **useRestClientVue(config)** реактивный REST клиент (computed)
899
+ | Функция | Возвращает | Описание |
900
+ |---------|-----------|----------|
901
+ | `usePipelineProgressVue(orchestrator)` | `Ref<PipelineProgress>` | Реактивный прогресс |
902
+ | `usePipelineRunVue(orchestrator)` | `{ run, running, result, error, stageResults, abort, pause, resume, rerunStep, clearStageResults }` | Запуск и реактивное состояние |
903
+ | `usePipelineStepEventVue(orchestrator, stepKey, eventType)` | `Ref<any>` | Последний payload события шага |
904
+ | `usePipelineLogsVue(orchestrator)` | `Ref<log[]>` | Реактивные логи |
905
+ | `useRerunPipelineStepVue(orchestrator)` | `function` | Привязанный `rerunStep` |
906
+ | `useRestClientVue(config)` | `ComputedRef<RestClient>` | Реактивный REST-клиент |
852
907
 
853
908
  ---
854
909
 
855
- ### Расширение для React
856
-
857
- #### Пример: использование в React компоненте
910
+ ### Интеграция с React
858
911
 
859
912
  ```jsx
860
- import React from "react";
861
913
  import {
862
914
  PipelineOrchestrator,
863
915
  usePipelineProgressReact,
864
916
  usePipelineRunReact,
865
917
  } from "rest-pipeline-js/react";
866
918
 
867
- const pipelineConfig = {
868
- stages: [
869
- /* ... */
870
- ],
871
- };
872
- const httpConfig = { baseURL: "https://api.example.com" };
873
- const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
919
+ const orchestrator = new PipelineOrchestrator({ config: { stages: [/* ... */] } });
874
920
 
875
921
  export function PipelineComponent() {
876
922
  const progress = usePipelineProgressReact(orchestrator);
877
- const [run, { running, result, error }] = usePipelineRunReact(orchestrator);
923
+ const [run, { running, result, error, abort, pause, resume, rerunStep }] =
924
+ usePipelineRunReact(orchestrator);
878
925
 
879
926
  return (
880
927
  <div>
881
928
  <div>Текущий шаг: {progress.currentStage}</div>
882
- <button onClick={() => run()} disabled={running}>
883
- Старт
884
- </button>
929
+ <button onClick={() => run()} disabled={running}>Старт</button>
930
+ <button onClick={() => abort()} disabled={!running}>Отмена</button>
931
+ <button onClick={() => pause()}>Пауза</button>
932
+ <button onClick={() => resume()}>Продолжить</button>
885
933
  {result && <div>Готово: {JSON.stringify(result)}</div>}
886
934
  {error && <div>Ошибка: {error.message}</div>}
887
935
  </div>
@@ -889,49 +937,56 @@ export function PipelineComponent() {
889
937
  }
890
938
  ```
891
939
 
892
- ---
940
+ Хуки (импорт из `rest-pipeline-js/react`):
893
941
 
894
- Экспортируются хуки для интеграции rest-pipeline-js с React (импортировать из `rest-pipeline-js/react`):
895
-
896
- - **usePipelineProgressReact(orchestrator)** подписка на прогресс pipeline (PipelineProgress)
897
- - **usePipelineRunReact(orchestrator)** запуск pipeline и статусы ([run, { running, result, error }])
898
- - **usePipelineStepEventReact(orchestrator, stepKey, eventType)** подписка на события шага (success/error/progress)
899
- - **usePipelineLogsReact(orchestrator)** подписка на логи pipeline
900
- - **useRerunPipelineStepReact(orchestrator)** функция для повторного запуска шага
901
- - **useRestClientReact(config)** мемоизированный REST клиент
942
+ | Хук | Возвращает | Описание |
943
+ |-----|-----------|----------|
944
+ | `usePipelineProgressReact(orchestrator)` | `PipelineProgress` | Реактивный прогресс |
945
+ | `usePipelineRunReact(orchestrator)` | `[run, { running, result, error, stageResults, abort, pause, resume, rerunStep }]` | Запуск и состояние |
946
+ | `usePipelineStepEventReact(orchestrator, stepKey, eventType)` | `any` | Последний payload события шага |
947
+ | `usePipelineLogsReact(orchestrator)` | `log[]` | Реактивные логи |
948
+ | `useRerunPipelineStepReact(orchestrator)` | `function` | Привязанный `rerunStep` |
949
+ | `useRestClientReact(config)` | `RestClient` | Мемоизированный REST-клиент |
902
950
 
903
951
  ---
904
952
 
905
- ## Точки входа и импорты (русский)
906
-
907
- В пакете три точки входа, чтобы при использовании только ядра или Vue сборщик не подтягивал React.
953
+ ## Точки входа
908
954
 
909
955
  | Точка входа | Назначение | Содержимое |
910
956
  |-------------|------------|------------|
911
- | `rest-pipeline-js` | Только ядро | `PipelineOrchestrator`, `createRestClient`, типы, rest-client, request-executor, error-handler, progress-tracker. Без Vue/React. |
912
- | `rest-pipeline-js/vue` | Проекты на Vue | Всё из ядра + Vue-хуки: usePipelineProgressVue, usePipelineRunVue, useRestClientVue, usePipelineStepEventVue, usePipelineLogsVue, useRerunPipelineStepVue. |
913
- | `rest-pipeline-js/react` | Проекты на React | Всё из ядра + React-хуки: usePipelineProgressReact, usePipelineRunReact, useRestClientReact, usePipelineStepEventReact, usePipelineLogsReact, useRerunPipelineStepReact. |
957
+ | `rest-pipeline-js` | Только ядро | `PipelineOrchestrator`, `createRestClient`, типы. Без Vue/React. |
958
+ | `rest-pipeline-js/vue` | Vue-проекты | Ядро + Vue composition-функции |
959
+ | `rest-pipeline-js/react` | React-проекты | Ядро + React хуки |
960
+
961
+ ```js
962
+ // Только ядро
963
+ import { createRestClient, PipelineOrchestrator } from "rest-pipeline-js";
964
+
965
+ // Vue
966
+ import { PipelineOrchestrator, usePipelineRunVue } from "rest-pipeline-js/vue";
914
967
 
915
- Рекомендуемые импорты: ядро — `rest-pipeline-js`; Vue — `rest-pipeline-js/vue`; React — `rest-pipeline-js/react`. Пакет помечен как `sideEffects: false`; `react`/`react-dom` в `peerDependencies` для входа React.
968
+ // React
969
+ import { PipelineOrchestrator, usePipelineRunReact } from "rest-pipeline-js/react";
970
+ ```
971
+
972
+ `sideEffects: false` — неиспользуемые точки входа удаляются tree-shaking'ом. `react`/`react-dom` — `peerDependencies`.
916
973
 
917
974
  ---
918
975
 
919
976
  ## Требования
920
977
 
921
978
  - Node.js >= 14.0.0
922
- - Современный браузер с поддержкой ES2020
979
+ - Современный браузер с поддержкой ES2019+
923
980
 
924
981
  ---
925
982
 
926
- ## Разработка и вклад
983
+ ## Разработка
927
984
 
928
985
  ```bash
929
- # Клонировать репозиторий
930
986
  git clone https://github.com/macrulezru/pipeline-js.git
931
987
  cd pipeline-js
932
988
  npm install
933
989
  npm test
934
- npm run lint
935
990
  ```
936
991
 
937
992
  ---
@@ -946,12 +1001,6 @@ MIT
946
1001
 
947
1002
  Данил Лисин Владимирович aka Macrulez
948
1003
 
949
- GitHub: [macrulezru](https://github.com/macrulezru)
950
-
951
- Сайт: [macrulez.ru](https://macrulez.ru/)
952
-
953
- ---
954
-
955
- ## Поддержка
1004
+ GitHub: [macrulezru](https://github.com/macrulezru) · Сайт: [macrulez.ru](https://macrulez.ru/)
956
1005
 
957
1006
  Вопросы и баги — через [issue](https://github.com/macrulezru/pipeline-js/issues)