rest-pipeline-js 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,253 +1,409 @@
1
1
 
2
- # pipeline-js
3
2
 
4
- Модуль для работы с REST API, пайплайнами запросов и отслеживанием прогресса. Не зависит от Vue/React, но легко интегрируется в любые проекты.
5
3
 
4
+ ## Возможности и API
6
5
 
7
- ## Установка
8
6
 
9
- ```
10
- npm i rest-pipeline-js
7
+ ### Базовый модуль (rest-pipeline-js)
8
+
9
+ #### Пример: создание REST клиента и выполнение запроса
10
+
11
+ ```js
12
+ import { createRestClient } from 'rest-pipeline-js';
13
+
14
+ const client = createRestClient({
15
+ baseURL: 'https://api.example.com',
16
+ timeout: 5000,
17
+ headers: { Authorization: 'Bearer TOKEN' },
18
+ });
19
+
20
+ async function fetchUser(id) {
21
+ const res = await client.request(`/users/${id}`);
22
+ if (res.error) {
23
+ console.error(res.error);
24
+ } else {
25
+ console.log(res.data);
26
+ }
27
+ }
11
28
  ```
12
29
 
13
- ## Быстрый старт (чистый JS)
30
+
31
+ #### Пример: запуск pipeline, обработка ошибок, отслеживание выполнения и использование общего пула данных
14
32
 
15
33
  ```js
16
- const { createRestClient, PipelineOrchestrator } = require('pipeline-js');
34
+ import { PipelineOrchestrator } from 'rest-pipeline-js';
35
+
36
+ const pipelineConfig = {
37
+ steps: [
38
+ {
39
+ key: 'step1',
40
+ command: '/api/step1',
41
+ method: 'POST',
42
+ // Можно добавить кастомные параметры шага
43
+ },
44
+ {
45
+ key: 'step2',
46
+ command: '/api/step2',
47
+ method: 'POST',
48
+ dependsOn: ['step1'], // step2 выполнится только после step1
49
+ },
50
+ ],
51
+ };
52
+
53
+ const httpConfig = {
54
+ baseURL: 'https://api.example.com',
55
+ timeout: 7000,
56
+ headers: { Authorization: 'Bearer TOKEN' },
57
+ retry: { attempts: 2, delayMs: 1000 },
58
+ cache: { enabled: true, ttlMs: 60000 },
59
+ rateLimit: { maxConcurrent: 2 },
60
+ metrics: {
61
+ onRequestStart: info => console.log('Start:', info),
62
+ onRequestEnd: info => console.log('End:', info),
63
+ },
64
+ };
65
+
66
+ // Общий пул данных между шагами
67
+ const sharedData = { sessionId: 'abc123' };
68
+
69
+ const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig, sharedData, { autoReset: true });
70
+
71
+ // Отслеживание прогресса
72
+ orchestrator.subscribeProgress(progress => {
73
+ console.log('Текущий шаг:', progress.currentStage, 'Статусы:', progress.stageStatuses);
74
+ });
17
75
 
18
- // Создание REST клиента
19
- const client = createRestClient({ baseURL: 'https://api.example.com' });
20
- client.get('/endpoint').then(response => {
21
- console.log(response.data);
76
+ // Подписка на события успеха/ошибки шага
77
+ orchestrator.on('step:step1:success', payload => {
78
+ console.log('Step 1 завершён успешно:', payload.data);
79
+ });
80
+ orchestrator.on('step:step2:error', payload => {
81
+ console.error('Ошибка на step2:', payload.error);
22
82
  });
23
83
 
24
- // Пример пайплайна
25
- const pipeline = new PipelineOrchestrator({
26
- stages: [
27
- {
28
- key: 'getUser',
29
- request: async () => client.get('/user'),
30
- },
31
- {
32
- key: 'getPosts',
33
- request: async (_, results) => client.get(`/posts?userId=${results[0].data.id}`),
34
- },
35
- ],
36
- }, { baseURL: 'https://api.example.com' });
37
-
38
- // Подписка на прогресс
39
- const unsubscribe = pipeline.subscribeProgress(progress => {
40
- console.log('Pipeline progress:', progress);
84
+ // Подписка на все логи pipeline
85
+ orchestrator.on('log', () => {
86
+ console.log('Логи:', orchestrator.getLogs());
41
87
  });
42
88
 
43
- // Получить текущий прогресс (snapshot)
44
- console.log(pipeline.getProgress());
89
+ // Запуск pipeline с передачей параметров
90
+ orchestrator.run({ foo: 'bar' })
91
+ .then(result => {
92
+ console.log('Pipeline завершён. Итог:', result);
93
+ // Доступ к результатам всех шагов:
94
+ console.log('Результаты шагов:', result.stageResults);
95
+ })
96
+ .catch(err => {
97
+ // Глобальная обработка ошибок pipeline
98
+ console.error('Pipeline error:', err);
99
+ });
100
+
101
+ // Повторный запуск шага (например, после ошибки)
102
+ // orchestrator.rerunStep('step2');
45
103
  ```
46
104
 
105
+ ---
47
106
 
107
+ ---
48
108
 
49
- ## React: хуки для интеграции
109
+ Модуль предоставляет универсальный механизм для построения и управления REST API pipeline с поддержкой прогресса, обработки ошибок, подписки на события и расширяемости.
50
110
 
51
- ### usePipelineProgress
52
- ```jsx
53
- import { usePipelineProgress } from 'pipeline-js/react';
54
- import { PipelineOrchestrator } from 'pipeline-js';
55
111
 
56
- const pipeline = new PipelineOrchestrator({ stages: [...] }, { baseURL: '...' });
57
- const progress = usePipelineProgress(pipeline);
58
- ```
112
+ #### Основные классы и функции
59
113
 
60
- ### usePipelineRun
61
- ```jsx
62
- import { usePipelineRun } from 'pipeline-js/react';
63
- const [run, { running, result, error }] = usePipelineRun(pipeline);
64
-
65
- // В компоненте:
66
- <button onClick={() => run()}>Старт</button>
67
- {running && <span>Выполняется...</span>}
68
- {result && <pre>{JSON.stringify(result)}</pre>}
69
- {error && <span style={{color:'red'}}>{String(error)}</span>}
70
- ```
114
+ ---
71
115
 
72
- ### useRestClient
73
- ```jsx
74
- import { useRestClient } from 'pipeline-js/react';
75
- const api = useRestClient({ baseURL: '...' });
76
- ```
77
116
 
78
- ## Vue: composition-функции для интеграции
117
+ ### createRestClient(config: HttpConfig): RestClient
118
+ Создаёт REST-клиент с поддержкой расширенных возможностей для работы с HTTP API.
79
119
 
80
- ### usePipelineProgress
120
+ #### Пример
81
121
  ```js
82
- import { usePipelineProgress } from 'pipeline-js/vue';
83
- import { PipelineOrchestrator } from 'pipeline-js';
122
+ import { createRestClient } from 'rest-pipeline-js';
123
+
124
+ const client = createRestClient({
125
+ baseURL: 'https://api.example.com',
126
+ timeout: 5000,
127
+ headers: { Authorization: 'Bearer TOKEN' },
128
+ retry: { attempts: 2 },
129
+ cache: { enabled: true, ttlMs: 60000 },
130
+ });
84
131
 
85
- const pipeline = new PipelineOrchestrator({ stages: [...] }, { baseURL: '...' });
86
- const progress = usePipelineProgress(pipeline);
132
+ async function getUser(id) {
133
+ const res = await client.request(`/users/${id}`);
134
+ if (res.error) {
135
+ console.error('Ошибка:', res.error);
136
+ } else {
137
+ console.log('Пользователь:', res.data);
138
+ }
139
+ }
87
140
  ```
88
141
 
89
- ### usePipelineRun
142
+
143
+ ---
144
+
145
+
146
+ ### RequestExecutor
147
+ Обёртка для выполнения REST-запросов с поддержкой автоматического retry и таймаута.
148
+
149
+ #### Пример
90
150
  ```js
91
- import { usePipelineRun } from 'pipeline-js/vue';
92
- const { run, running, result, error } = usePipelineRun(pipeline);
151
+ import { RequestExecutor } from 'rest-pipeline-js';
152
+
153
+ const executor = new RequestExecutor({ baseURL: 'https://api.example.com' });
154
+
155
+ async function fetchData() {
156
+ try {
157
+ const res = await executor.execute('/data', { method: 'GET' }, 3, 3000);
158
+ if (res.error) {
159
+ console.error('Ошибка:', res.error);
160
+ } else {
161
+ console.log('Данные:', res.data);
162
+ }
163
+ } catch (err) {
164
+ console.error('Критическая ошибка:', err);
165
+ }
166
+ }
93
167
  ```
94
168
 
95
- ### useRestClient
169
+
170
+ ---
171
+
172
+
173
+
174
+ ### PipelineOrchestrator
175
+ Основной класс для построения и управления конвейером (pipeline) из последовательных шагов.
176
+
177
+ #### Основные методы и параметры
178
+
179
+ - **constructor(pipelineConfig, httpConfig, sharedData?, options?)** — создание экземпляра:
180
+ - `pipelineConfig` — массив шагов (steps), их параметры, условия, обработчики
181
+ - `httpConfig` — настройки HTTP клиента
182
+ - `sharedData` — общий пул данных между шагами
183
+ - `options.autoReset` — сбрасывать ли состояние после завершения
184
+
185
+ - **run(onStepPause?, externalSignal?)** — запуск конвейера
186
+ - `onStepPause(stepIndex, stepResult, stageResults)` — callback для паузы/подтверждения/модификации результата между шагами (можно реализовать задержку, диалог, логику)
187
+ - `externalSignal` — внешний AbortSignal для отмены
188
+ - Возвращает: `{ stageResults, success }`
189
+
190
+ - **rerunStep(stepKey, options?)** — повторно выполнить один шаг
191
+ - `onStepPause` и `externalSignal` аналогично run
192
+ - Возвращает результат шага
193
+
194
+ - **subscribeProgress(listener)** — подписка на прогресс выполнения (listener получает PipelineProgress)
195
+ - **subscribeStageResults(listener)** — подписка на изменения результатов всех шагов
196
+ - **subscribeStepProgress(stepKey, listener)** — подписка на прогресс конкретного шага
197
+ - **on(eventName, handler)** — универсальная подписка на события:
198
+ - `step:<stepKey>:start|success|error|progress` — события по шагам
199
+ - `log` — новые логи
200
+ - любые кастомные события
201
+ - **onStepStart/Finish/Error(handler)** — подписка на начало/успех/ошибку шага (PipelineStepEvent)
202
+ - **getProgress()** — получить текущий прогресс (snapshot)
203
+ - **getProgressRef()** — получить ссылку на объект прогресса (для реактивности)
204
+ - **getLogs()** — получить массив логов pipeline
205
+ - **abort()** — отменить выполнение пайплайна
206
+ - **isAborted()** — проверить, был ли пайплайн отменён
207
+
208
+ #### Важные параметры шага (stage):
209
+ - `key` — уникальный ключ шага
210
+ - `command` — команда/endpoint для запроса
211
+ - `method` — HTTP-метод
212
+ - `dependsOn` — массив ключей шагов, от которых зависит этот шаг
213
+ - `condition(prev, allResults, sharedData)` — функция-условие для выполнения шага
214
+ - `request(prev, allResults)` — кастомная функция запроса (альтернатива command)
215
+ - `retryCount`, `timeoutMs` — индивидуальные настройки повтора и таймаута
216
+ - `errorHandler(error, key, sharedData)` — обработчик ошибок шага
217
+
218
+ #### Пример
96
219
  ```js
97
- import { useRestClient } from 'pipeline-js/vue';
98
- const api = useRestClient({ baseURL: '...' });
220
+ import { PipelineOrchestrator } from 'rest-pipeline-js';
221
+
222
+ const pipelineConfig = {
223
+ steps: [
224
+ { key: 'first', command: '/api/first', method: 'POST' },
225
+ { key: 'second', command: '/api/second', method: 'POST', dependsOn: ['first'] },
226
+ ],
227
+ };
228
+ const httpConfig = { baseURL: 'https://api.example.com' };
229
+ const sharedData = { sessionId: 'abc' };
230
+ const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig, sharedData);
231
+
232
+ orchestrator.subscribeProgress(progress => {
233
+ console.log('Прогресс:', progress);
234
+ });
235
+
236
+ orchestrator.on('step:first:success', payload => {
237
+ console.log('Первый шаг выполнен:', payload.data);
238
+ });
239
+
240
+ // Пауза 1 секунда между шагами
241
+ orchestrator.run(async (i, result) => { await new Promise(r => setTimeout(r, 1000)); return result; })
242
+ .then(result => console.log('Pipeline завершён:', result))
243
+ .catch(err => console.error('Ошибка pipeline:', err));
99
244
  ```
100
245
 
101
- ## Использование в Vue 3
102
246
 
247
+ ---
248
+
249
+
250
+ ### ProgressTracker
251
+ Внутренний класс для отслеживания прогресса pipeline.
252
+
253
+ #### Пример
103
254
  ```js
104
- import { createRestClient, PipelineOrchestrator } from 'pipeline-js';
105
- import { ref, onUnmounted } from 'vue';
106
-
107
- const client = createRestClient({ baseURL: 'https://api.example.com' });
108
- const pipeline = new PipelineOrchestrator({
109
- stages: [
110
- // ...
111
- ],
112
- }, { baseURL: 'https://api.example.com' });
113
-
114
- const progress = ref(pipeline.getProgress());
115
- const unsubscribe = pipeline.subscribeProgress(p => {
116
- progress.value = p;
255
+ import { ProgressTracker } from 'rest-pipeline-js';
256
+
257
+ const tracker = new ProgressTracker(3); // 3 шага
258
+ tracker.subscribe(progress => {
259
+ console.log('Текущий прогресс:', progress);
117
260
  });
118
- onUnmounted(unsubscribe);
261
+ tracker.updateStage(1, 'success');
262
+ console.log(tracker.getProgress());
119
263
  ```
120
264
 
121
- ## Основные API
122
265
 
123
- ### RestClient
124
- - `createRestClient(config)` — создать клиента (axios-like API: get, post, request и др.)
266
+ ---
125
267
 
126
- ### PipelineOrchestrator
127
- - `new PipelineOrchestrator(pipelineConfig, httpConfig)` — создать пайплайн
128
- - `subscribeProgress(listener)` — подписка на прогресс, возвращает функцию отписки
129
- - `getProgress()` — получить текущий прогресс (snapshot)
130
-
131
- ### ProgressTracker (внутренний)
132
- - Реактивность реализована через подписки (observer pattern), не зависит от Vue/React
133
-
134
- ## Структура
135
- - src/rest-client.ts — основной REST клиент
136
- - src/types.ts — типы
137
- - src/request-executor.ts — выполнение запросов
138
- - src/error-handler.ts — обработка ошибок
139
- - src/progress-tracker.ts — отслеживание прогресса
140
- - src/pipeline-orchestrator.ts — оркестрация пайплайна
141
-
142
- ### POST запрос
143
-
144
- ```javascript
145
- api.post('/users', {
146
- name: 'John Doe',
147
- email: 'john@example.com'
148
- })
149
- .then(response => {
150
- console.log('User created:', response.data);
151
- })
152
- .catch(error => {
153
- console.error('Error:', error);
154
- });
155
- ```
156
268
 
157
- ### Использование с async/await
158
-
159
- ```javascript
160
- async function getUsers() {
161
- try {
162
- const response = await api.get('/users');
163
- return response.data;
164
- } catch (error) {
165
- console.error('Error fetching users:', error);
166
- throw error;
167
- }
168
- }
269
+ ### ErrorHandler
270
+ Класс для обработки ошибок шагов pipeline.
271
+
272
+ #### Пример
273
+ ```js
274
+ import { ErrorHandler } from 'rest-pipeline-js';
275
+
276
+ const handler = new ErrorHandler();
277
+ const error = handler.handle(new Error('fail'), 'step1');
278
+ console.log(error); // { type: 'unknown', error: [Error], stageKey: 'step1' }
169
279
  ```
170
280
 
171
- ## API
281
+ #### Типы и интерфейсы:
282
+
283
+ - **HttpConfig** — конфигурация REST клиента (baseURL, timeout, headers, retry, cache, rateLimit, metrics)
284
+ - **ApiError** — описание ошибки API
285
+ - **ApiResponse<T>** — ответ API (данные, ошибка, статус)
286
+ - **PipelineConfig, PipelineResult, PipelineStepEvent, PipelineStepStatus** — описание pipeline и шагов
287
+
288
+ ---
289
+
290
+
291
+ ### Расширение для Vue
172
292
 
173
- ### Конструктор
293
+ #### Пример: использование во Vue компоненте
174
294
 
175
- ```javascript
176
- new Pipeline(config)
295
+ ```js
296
+ <script setup>
297
+ import { ref } from 'vue';
298
+ import { PipelineOrchestrator } from 'rest-pipeline-js';
299
+ import { usePipelineProgress, usePipelineRun } from 'rest-pipeline-js/vue';
300
+
301
+ const pipelineConfig = { steps: [/* ... */] };
302
+ const httpConfig = { baseURL: 'https://api.example.com' };
303
+ const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
304
+
305
+ const progress = usePipelineProgress(orchestrator);
306
+ const { run, running, result, error } = usePipelineRun(orchestrator);
307
+ </script>
308
+
309
+ <template>
310
+ <div>
311
+ <div>Текущий шаг: {{ progress.value.currentStage }}</div>
312
+ <button @click="run()" :disabled="running">Старт</button>
313
+ <div v-if="result">Готово: {{ result }}</div>
314
+ <div v-if="error">Ошибка: {{ error.message }}</div>
315
+ </div>
316
+ </template>
177
317
  ```
178
318
 
179
- **Параметры:**
180
- - `config` (Object) - Объект конфигурации
181
- - `baseURL` (String) - Базовый URL для всех запросов
182
- - `timeout` (Number) - Таймаут запроса в миллисекундах
183
- - `headers` (Object) - Заголовки по умолчанию
319
+ ---
320
+
321
+ Экспортируются composition-функции для интеграции rest-pipeline-js с Vue 3:
322
+
323
+ - **usePipelineProgress(orchestrator)** реактивный прогресс pipeline (Ref<PipelineProgress>)
324
+ - **usePipelineRun(orchestrator)** — запуск pipeline и реактивные статусы (run, running, result, error)
325
+ - **usePipelineStepEvent(orchestrator, stepKey, eventType)** — подписка на события шага (успех, ошибка, прогресс)
326
+ - **usePipelineLogs(orchestrator)** — реактивные логи pipeline
327
+ - **useRerunPipelineStep(orchestrator)** — функция для повторного запуска шага
328
+ - **useRestClient(config)** — реактивный REST клиент (computed)
329
+
330
+ ---
184
331
 
185
- ### Методы
186
332
 
187
- #### `get(url, config)`
188
- Выполняет GET запрос.
333
+ ### Расширение для React
189
334
 
190
- #### `post(url, data, config)`
191
- Выполняет POST запрос.
335
+ #### Пример: использование в React компоненте
192
336
 
193
- #### `put(url, data, config)`
194
- Выполняет PUT запрос.
337
+ ```jsx
338
+ import React from 'react';
339
+ import { PipelineOrchestrator } from 'rest-pipeline-js';
340
+ import { usePipelineProgress, usePipelineRun } from 'rest-pipeline-js/react';
341
+
342
+ const pipelineConfig = { steps: [/* ... */] };
343
+ const httpConfig = { baseURL: 'https://api.example.com' };
344
+ const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
345
+
346
+ export function PipelineComponent() {
347
+ const progress = usePipelineProgress(orchestrator);
348
+ const [run, { running, result, error }] = usePipelineRun(orchestrator);
349
+
350
+ return (
351
+ <div>
352
+ <div>Текущий шаг: {progress.currentStage}</div>
353
+ <button onClick={() => run()} disabled={running}>Старт</button>
354
+ {result && <div>Готово: {JSON.stringify(result)}</div>}
355
+ {error && <div>Ошибка: {error.message}</div>}
356
+ </div>
357
+ );
358
+ }
359
+ ```
195
360
 
196
- #### `delete(url, config)`
197
- Выполняет DELETE запрос.
361
+ ---
198
362
 
199
- #### `patch(url, data, config)`
200
- Выполняет PATCH запрос.
363
+ Экспортируются хуки для интеграции rest-pipeline-js с React:
201
364
 
202
- ## Особенности
365
+ - **usePipelineProgress(orchestrator)** — подписка на прогресс pipeline (PipelineProgress)
366
+ - **usePipelineRun(orchestrator)** — запуск pipeline и статусы ([run, { running, result, error }])
367
+ - **usePipelineStepEvent(orchestrator, stepKey, eventType)** — подписка на события шага (success/error/progress)
368
+ - **usePipelineLogs(orchestrator)** — подписка на логи pipeline
369
+ - **useRerunPipelineStep(orchestrator)** — функция для повторного запуска шага
370
+ - **useRestClient(config)** — мемоизированный REST клиент
203
371
 
204
- - ✅ Поддержка Promise и async/await
205
- - ✅ Автоматическая обработка JSON
206
- - ✅ Настраиваемые заголовки
207
- - ✅ Обработка ошибок
208
- - ✅ Поддержка таймаутов
209
- - ✅ Легковесная библиотека без лишних зависимостей
372
+ ---
210
373
 
211
374
  ## Требования
212
375
 
213
- - Node.js >= 12.0.0
214
- - Современный браузер с поддержкой ES6
376
+ - Node.js >= 14.0.0
377
+ - Современный браузер с поддержкой ES2020
378
+
379
+ ---
215
380
 
216
- ## Разработка
381
+ ## Разработка и вклад
217
382
 
218
383
  ```bash
219
384
  # Клонировать репозиторий
220
385
  git clone https://github.com/macrulezru/pipeline-js.git
221
-
222
- # Установить зависимости
386
+ cd pipeline-js
223
387
  npm install
224
-
225
- # Запустить тесты
226
388
  npm test
227
-
228
- # Запустить линтер
229
389
  npm run lint
230
390
  ```
231
391
 
232
- ## Вклад в проект
233
-
234
- Мы приветствуем вклад в развитие проекта! Пожалуйста:
235
-
236
- 1. Форкните репозиторий
237
- 2. Создайте ветку для вашей функции (`git checkout -b feature/amazing-feature`)
238
- 3. Закоммитьте изменения (`git commit -m 'Add some amazing feature'`)
239
- 4. Запушьте ветку (`git push origin feature/amazing-feature`)
240
- 5. Откройте Pull Request
392
+ ---
241
393
 
242
394
  ## Лицензия
243
395
 
244
396
  MIT
245
397
 
398
+ ---
399
+
246
400
  ## Автор
247
401
 
248
- Git [macrulezru](https://github.com/macrulezru)
249
- Сайт [macrulez.ru](https://macrulez.ru/)
402
+ GitHub: [macrulezru](https://github.com/macrulezru)
403
+ Сайт: [macrulez.ru](https://macrulez.ru/)
404
+
405
+ ---
250
406
 
251
407
  ## Поддержка
252
408
 
253
- Если у вас возникли вопросы или проблемы, пожалуйста, создайте [issue](https://github.com/macrulezru/pipeline-js/issues) в репозитории.
409
+ Вопросы и баги через [issue](https://github.com/macrulezru/pipeline-js/issues)
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "rest-pipeline-js",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Pipeline Orchestration Utilities for JavaScript REST API Clients",
5
5
  "main": "dist/index.js",
6
- "types": "src/types.ts",
6
+ "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
9
  "test": "echo \"No tests specified\" && exit 0"
@@ -0,0 +1,45 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { PipelineOrchestrator } from '../src/pipeline-orchestrator';
3
+
4
+ /**
5
+ * React hook for subscribing to step events (success/error/progress) for a specific step
6
+ * @param orchestrator PipelineOrchestrator instance
7
+ * @param stepKey string — step key
8
+ * @param eventType 'success' | 'error' | 'progress'
9
+ * @returns last event payload (any)
10
+ */
11
+ export function usePipelineStepEvent(orchestrator: PipelineOrchestrator, stepKey: string, eventType: 'success' | 'error' | 'progress') {
12
+ const [event, setEvent] = useState<any>(null);
13
+ const handlerRef = useRef<any>();
14
+ useEffect(() => {
15
+ handlerRef.current = (payload: any) => setEvent(payload);
16
+ const eventName = `step:${stepKey}:${eventType}`;
17
+ const unsubscribe = orchestrator.on(eventName, handlerRef.current);
18
+ return () => unsubscribe && unsubscribe();
19
+ }, [orchestrator, stepKey, eventType]);
20
+ return event;
21
+ }
22
+
23
+ /**
24
+ * React hook for subscribing to pipeline logs
25
+ * @param orchestrator PipelineOrchestrator instance
26
+ * @returns array of log entries (reactive)
27
+ */
28
+ export function usePipelineLogs(orchestrator: PipelineOrchestrator) {
29
+ const [logs, setLogs] = useState(() => orchestrator.getLogs());
30
+ useEffect(() => {
31
+ const handler = () => setLogs(orchestrator.getLogs());
32
+ const unsubscribe = orchestrator.on('log', handler);
33
+ return () => unsubscribe && unsubscribe();
34
+ }, [orchestrator]);
35
+ return logs;
36
+ }
37
+
38
+ /**
39
+ * React hook for rerunning a pipeline step
40
+ * @param orchestrator PipelineOrchestrator instance
41
+ * @returns rerunStep function
42
+ */
43
+ export function useRerunPipelineStep(orchestrator: PipelineOrchestrator) {
44
+ return orchestrator.rerunStep.bind(orchestrator);
45
+ }
@@ -1,10 +1,32 @@
1
+
1
2
  import { ErrorHandler } from './error-handler';
2
3
  import { ProgressTracker } from './progress-tracker';
3
4
  import { RequestExecutor } from './request-executor';
4
5
 
5
6
  import type { PipelineConfig, PipelineResult } from './types';
6
7
 
7
- // import { metricsBus } from '@/core/metrics/metrics-bus';
8
+ /**
9
+ * Событие шага pipeline (для хуков)
10
+ */
11
+ export type PipelineStepEvent = {
12
+ /** Индекс шага */
13
+ stepIndex: number;
14
+ /** Ключ шага */
15
+ stepKey: string;
16
+ /** Статус шага */
17
+ status: import('./types').PipelineStepStatus;
18
+ /** Данные результата (если успех) */
19
+ data?: any;
20
+ /** Ошибка (если error) */
21
+ error?: import('./types').ApiError;
22
+ /** Снимок всех результатов на момент события */
23
+ stageResults: Record<string, import('./types').PipelineStepResult>;
24
+ };
25
+
26
+ /**
27
+ * Callback для подписки на события этапов pipeline
28
+ */
29
+ export type PipelineStepEventHandler = (event: PipelineStepEvent) => void | Promise<void>;
8
30
 
9
31
  export class PipelineOrchestrator {
10
32
  private progress: ProgressTracker;
@@ -12,15 +34,236 @@ export class PipelineOrchestrator {
12
34
  private executor: RequestExecutor;
13
35
  private sharedData: Record<string, unknown>;
14
36
 
37
+ private onStepStartHandlers: PipelineStepEventHandler[] = [];
38
+ private onStepFinishHandlers: PipelineStepEventHandler[] = [];
39
+ private onStepErrorHandlers: PipelineStepEventHandler[] = [];
40
+
41
+ /** Универсальные подписчики событий: ключ — имя события */
42
+ private eventHandlers: Record<string, Array<(...args: any[]) => void | Promise<void>>> = {};
43
+
44
+ /** Встроенные логи */
45
+ private logs: Array<{ type: string; message: string; data?: any; timestamp: Date }> = [];
46
+
47
+ private stageResults: Record<string, import('./types').PipelineStepResult> = {};
48
+ private stageResultsListeners: Array<(results: Record<string, import('./types').PipelineStepResult>) => void> = [];
49
+ private autoReset: boolean;
50
+ /** AbortController для отмены пайплайна */
51
+ private abortController: AbortController | null = null;
52
+
53
+ private config: PipelineConfig;
54
+
15
55
  constructor(
16
- private config: PipelineConfig,
56
+ config: PipelineConfig,
17
57
  httpConfig: import('./types').HttpConfig,
18
58
  sharedData: Record<string, unknown> = {},
59
+ options: { autoReset?: boolean } = {}
19
60
  ) {
61
+ this.config = config;
20
62
  this.progress = new ProgressTracker(config.stages.length);
21
63
  this.errorHandler = new ErrorHandler();
22
64
  this.executor = new RequestExecutor(httpConfig);
23
65
  this.sharedData = sharedData;
66
+ this.autoReset = options.autoReset ?? false;
67
+ }
68
+
69
+ /**
70
+ * Подписка на изменения stageResults (реактивно)
71
+ */
72
+ subscribeStageResults(listener: (results: Record<string, import('./types').PipelineStepResult>) => void) {
73
+ this.stageResultsListeners.push(listener);
74
+ // Немедленно уведомляем нового подписчика о текущем состоянии
75
+ listener({ ...this.stageResults });
76
+ return () => {
77
+ this.stageResultsListeners = this.stageResultsListeners.filter(l => l !== listener);
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Универсальная подписка на события: step:<key>, progress, log и др.
83
+ */
84
+ on(event: string, handler: (...args: any[]) => void | Promise<void>) {
85
+ if (!this.eventHandlers[event]) this.eventHandlers[event] = [];
86
+ this.eventHandlers[event].push(handler);
87
+ return () => {
88
+ this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler);
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Вызов всех обработчиков события
94
+ */
95
+ private async emit(event: string, ...args: any[]) {
96
+ if (this.eventHandlers[event]) {
97
+ for (const handler of this.eventHandlers[event]) {
98
+ await handler(...args);
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Получить логи пайплайна
105
+ */
106
+ getLogs() {
107
+ return [...this.logs];
108
+ }
109
+
110
+ private notifyStageResults() {
111
+ for (const listener of this.stageResultsListeners) {
112
+ listener({ ...this.stageResults });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Повторно выполнить только один шаг пайплайна (без полного рестарта)
118
+ * @param stepKey ключ шага
119
+ * @param options дополнительные опции (например, onStepPause, externalSignal)
120
+ */
121
+ async rerunStep(
122
+ stepKey: string,
123
+ options?: {
124
+ onStepPause?: (
125
+ stepIndex: number,
126
+ stepResult: unknown,
127
+ stageResults: Record<string, import('./types').PipelineStepResult>,
128
+ ) => Promise<unknown> | unknown,
129
+ externalSignal?: AbortSignal
130
+ }
131
+ ): Promise<import('./types').PipelineStepResult | undefined> {
132
+ const i = this.config.stages.findIndex(s => s.key === stepKey);
133
+ if (i === -1) return undefined;
134
+ const stage = this.config.stages[i];
135
+ const key = stage.key;
136
+ const signal = options?.externalSignal;
137
+ this.logs.push({ type: 'log', message: `rerunStep:${key}:start`, timestamp: new Date(), data: { stepIndex: i } });
138
+ await this.emit('log', { type: 'rerunStep:start', stepKey: key, stepIndex: i });
139
+ this.stageResults[key] = { status: 'pending' };
140
+ this.notifyStageResults();
141
+ this.progress.updateStage(i, 'loading');
142
+ await this.emitStepStart({ stepIndex: i, stepKey: key, status: 'loading', stageResults: { ...this.stageResults } });
143
+ await this.emit(`step:${key}:start`, { stepIndex: i, stepKey: key, status: 'loading', stageResults: { ...this.stageResults } });
144
+ try {
145
+ let stepResult: unknown;
146
+ if (typeof stage.request === 'function') {
147
+ stepResult = await stage.request(
148
+ i > 0 ? this.stageResults[this.config.stages[i-1].key]?.data : undefined,
149
+ this.stageResults
150
+ );
151
+ } else {
152
+ const res = await this.executor.execute(
153
+ stage.key,
154
+ undefined,
155
+ stage.retryCount,
156
+ stage.timeoutMs,
157
+ );
158
+ stepResult = res.data;
159
+ }
160
+ if (options?.onStepPause) {
161
+ stepResult = await options.onStepPause(i, stepResult, this.stageResults);
162
+ }
163
+ this.stageResults[key] = { status: 'success', data: stepResult };
164
+ this.notifyStageResults();
165
+ this.progress.updateStage(i, 'success');
166
+ await this.emitStepFinish({ stepIndex: i, stepKey: key, status: 'success', data: stepResult, stageResults: { ...this.stageResults } });
167
+ await this.emit(`step:${key}:success`, { stepIndex: i, stepKey: key, status: 'success', data: stepResult, stageResults: { ...this.stageResults } });
168
+ this.logs.push({ type: 'log', message: `rerunStep:${key}:success`, timestamp: new Date(), data: { stepIndex: i, data: stepResult } });
169
+ await this.emit('log', { type: 'rerunStep:success', stepKey: key, stepIndex: i, data: stepResult });
170
+ return this.stageResults[key];
171
+ } catch (err) {
172
+ let handled;
173
+ if (stage && typeof stage.errorHandler === 'function') {
174
+ handled = stage.errorHandler(err, stage.key, this.sharedData);
175
+ } else if (stage) {
176
+ handled = this.errorHandler.handle(err, stage.key);
177
+ } else {
178
+ handled = this.errorHandler.handle(err, 'unknown');
179
+ }
180
+ if (!handled && stage) {
181
+ handled = this.errorHandler.handle(err, stage.key);
182
+ }
183
+ const { toApiError } = await import('./rest-client.js');
184
+ const apiError = toApiError(handled ?? err);
185
+ this.stageResults[key] = { status: 'error', error: apiError };
186
+ this.notifyStageResults();
187
+ this.progress.updateStage(i, 'error');
188
+ await this.emitStepError({ stepIndex: i, stepKey: key, status: 'error', error: apiError, stageResults: { ...this.stageResults } });
189
+ await this.emit(`step:${key}:error`, { stepIndex: i, stepKey: key, status: 'error', error: apiError, stageResults: { ...this.stageResults } });
190
+ this.logs.push({ type: 'error', message: `rerunStep:${key}:error`, timestamp: new Date(), data: { stepIndex: i, error: apiError } });
191
+ await this.emit('log', { type: 'rerunStep:error', stepKey: key, stepIndex: i, error: apiError });
192
+ return this.stageResults[key];
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Отменить выполнение пайплайна (вызывает ошибку AbortError)
198
+ */
199
+ abort() {
200
+ if (this.abortController) {
201
+ this.abortController.abort();
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Проверить, был ли пайплайн отменён
207
+ */
208
+ isAborted() {
209
+ return this.abortController?.signal.aborted ?? false;
210
+ }
211
+
212
+ /**
213
+ * Подписка на событие начала шага
214
+ */
215
+ onStepStart(handler: PipelineStepEventHandler) {
216
+ this.onStepStartHandlers.push(handler);
217
+ return () => {
218
+ this.onStepStartHandlers = this.onStepStartHandlers.filter(h => h !== handler);
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Подписка на событие успешного завершения шага
224
+ */
225
+ onStepFinish(handler: PipelineStepEventHandler) {
226
+ this.onStepFinishHandlers.push(handler);
227
+ return () => {
228
+ this.onStepFinishHandlers = this.onStepFinishHandlers.filter(h => h !== handler);
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Подписка на событие ошибки шага
234
+ */
235
+ onStepError(handler: PipelineStepEventHandler) {
236
+ this.onStepErrorHandlers.push(handler);
237
+ return () => {
238
+ this.onStepErrorHandlers = this.onStepErrorHandlers.filter(h => h !== handler);
239
+ };
240
+ }
241
+
242
+ private async emitStepStart(event: PipelineStepEvent) {
243
+ for (const handler of this.onStepStartHandlers) {
244
+ await handler(event);
245
+ }
246
+ // Гибкая подписка на step:<key>:start
247
+ await this.emit(`step:${event.stepKey}:start`, event);
248
+ // Логирование
249
+ this.logs.push({ type: 'log', message: `step:${event.stepKey}:start`, timestamp: new Date(), data: event });
250
+ await this.emit('log', { type: 'step:start', ...event });
251
+ }
252
+ private async emitStepFinish(event: PipelineStepEvent) {
253
+ for (const handler of this.onStepFinishHandlers) {
254
+ await handler(event);
255
+ }
256
+ await this.emit(`step:${event.stepKey}:success`, event);
257
+ this.logs.push({ type: 'log', message: `step:${event.stepKey}:success`, timestamp: new Date(), data: event });
258
+ await this.emit('log', { type: 'step:success', ...event });
259
+ }
260
+ private async emitStepError(event: PipelineStepEvent) {
261
+ for (const handler of this.onStepErrorHandlers) {
262
+ await handler(event);
263
+ }
264
+ await this.emit(`step:${event.stepKey}:error`, event);
265
+ this.logs.push({ type: 'error', message: `step:${event.stepKey}:error`, timestamp: new Date(), data: event });
266
+ await this.emit('log', { type: 'step:error', ...event });
24
267
  }
25
268
 
26
269
  /**
@@ -32,6 +275,13 @@ export class PipelineOrchestrator {
32
275
  return this.progress.subscribe(listener);
33
276
  }
34
277
 
278
+ /**
279
+ * Подписка на прогресс с фильтрацией по этапу (stepKey) или общий
280
+ */
281
+ subscribeStepProgress(stepKey: string, listener: (status: import('./types').PipelineStepStatus) => void) {
282
+ return this.on(`step:${stepKey}:progress`, listener);
283
+ }
284
+
35
285
  /**
36
286
  * Получить текущий прогресс выполнения pipeline (snapshot, не реактивный)
37
287
  */
@@ -40,44 +290,101 @@ export class PipelineOrchestrator {
40
290
  }
41
291
 
42
292
  /**
43
- * @param onStepPause
44
- * Необязательный callback, вызывается после каждого шага (до перехода к следующему).
45
- * Позволяет приостановить выполнение, запросить подтверждение пользователя или изменить результат шага.
46
- * Должен вернуть (optionally изменённый) результат шага или промис с ним.
47
- * Если не передан — пайплайн работает как раньше.
293
+ * Возвращает текущий снимок состояния прогресса (не реактивный).
294
+ * Для отслеживания изменений используйте subscribeProgress.
295
+ */
296
+ getProgressRef() {
297
+ return this.progress.getProgressRef();
298
+ }
299
+
300
+ /**
301
+ * Запустить выполнение пайплайна
302
+ * @param onStepPause callback для пользовательской паузы между шагами
303
+ * @param externalSignal внешний AbortSignal (опционально)
48
304
  */
49
305
  async run(
50
306
  onStepPause?: (
51
307
  stepIndex: number,
52
308
  stepResult: unknown,
53
- results: unknown[],
309
+ stageResults: Record<string, import('./types').PipelineStepResult>,
54
310
  ) => Promise<unknown> | unknown,
55
- ): Promise<PipelineResult> {
56
- const results: unknown[] = [];
57
- const errors: unknown[] = [];
311
+ externalSignal?: AbortSignal
312
+ ): Promise<import('./types').PipelineResult> {
313
+ if (this.autoReset) {
314
+ this.stageResults = {};
315
+ this.notifyStageResults();
316
+ }
58
317
  let success = true;
59
318
 
319
+ // Создаём новый AbortController для этого запуска
320
+ this.abortController = new AbortController();
321
+ const signal = externalSignal ?? this.abortController.signal;
322
+
60
323
  for (let i = 0; i < this.config.stages.length; i++) {
324
+ if (signal.aborted) {
325
+ // Прерываем выполнение, если был вызван abort
326
+ const { toApiError } = await import('./rest-client.js');
327
+ const apiError = toApiError({ message: 'Pipeline aborted', code: 'ABORTED' });
328
+ const key = this.config.stages[i]?.key || `stage${i}`;
329
+ this.stageResults[key] = { status: 'error', error: apiError };
330
+ this.notifyStageResults();
331
+ this.progress.updateStage(i, 'error');
332
+ this.logs.push({ type: 'error', message: `abort:${key}`, timestamp: new Date(), data: { stepIndex: i, error: apiError } });
333
+ await this.emit('log', { type: 'abort', stepKey: key, stepIndex: i, error: apiError });
334
+ await this.emitStepError({
335
+ stepIndex: i,
336
+ stepKey: key,
337
+ status: 'error',
338
+ error: apiError,
339
+ stageResults: { ...this.stageResults },
340
+ });
341
+ success = false;
342
+ break;
343
+ }
61
344
  const stage = this.config.stages[i];
62
- this.progress.updateStage(i, 'in-progress');
345
+ const key = stage?.key || `stage${i}`;
346
+ this.stageResults[key] = { status: 'pending' };
347
+ this.notifyStageResults();
348
+ this.progress.updateStage(i, 'loading');
349
+
350
+ // Гибкая подписка на прогресс шага
351
+ await this.emit(`step:${key}:progress`, 'loading');
352
+
353
+ // emit step start
354
+ await this.emitStepStart({
355
+ stepIndex: i,
356
+ stepKey: key,
357
+ status: 'loading',
358
+ stageResults: { ...this.stageResults },
359
+ });
63
360
 
64
361
  if (!stage) {
65
362
  this.progress.updateStage(i, 'skipped');
66
- results.push(undefined);
363
+ this.stageResults[key] = { status: 'skipped' };
364
+ this.notifyStageResults();
365
+ await this.emit(`step:${key}:progress`, 'skipped');
67
366
  continue;
68
367
  }
69
368
 
70
369
  // Проверка условия выполнения этапа
71
- if (stage.condition && !stage.condition(results[i - 1], results, this.sharedData)) {
370
+ if (stage.condition && !stage.condition(
371
+ i > 0 ? this.stageResults[this.config.stages[i-1].key]?.data : undefined,
372
+ this.stageResults,
373
+ this.sharedData)) {
72
374
  this.progress.updateStage(i, 'skipped');
73
- results.push(undefined);
375
+ this.stageResults[key] = { status: 'skipped' };
376
+ this.notifyStageResults();
377
+ await this.emit(`step:${key}:progress`, 'skipped');
74
378
  continue;
75
379
  }
76
380
  try {
77
381
  let stepResult: unknown;
78
382
  // Всегда передаём (prev, allResults) в request — best practice для pipeline
79
383
  if (typeof stage.request === 'function') {
80
- stepResult = await stage.request(results[i - 1], results);
384
+ stepResult = await stage.request(
385
+ i > 0 ? this.stageResults[this.config.stages[i-1].key]?.data : undefined,
386
+ this.stageResults
387
+ );
81
388
  } else if (stage.key) {
82
389
  const res = await this.executor.execute(
83
390
  stage.key,
@@ -92,12 +399,22 @@ export class PipelineOrchestrator {
92
399
 
93
400
  // --- Пользовательская пауза/подтверждение/изменение результата ---
94
401
  if (onStepPause) {
95
- stepResult = await onStepPause(i, stepResult, results);
402
+ stepResult = await onStepPause(i, stepResult, this.stageResults);
96
403
  }
97
- results.push(stepResult);
404
+ this.stageResults[key] = { status: 'success', data: stepResult };
405
+ this.notifyStageResults();
98
406
  this.progress.updateStage(i, 'success');
407
+ await this.emit(`step:${key}:progress`, 'success');
408
+
409
+ // emit step finish
410
+ await this.emitStepFinish({
411
+ stepIndex: i,
412
+ stepKey: key,
413
+ status: 'success',
414
+ data: stepResult,
415
+ stageResults: { ...this.stageResults },
416
+ });
99
417
 
100
- // ...existing code...
101
418
  } catch (err) {
102
419
  let handled;
103
420
  if (stage && typeof stage.errorHandler === 'function') {
@@ -110,13 +427,26 @@ export class PipelineOrchestrator {
110
427
  if (!handled && stage) {
111
428
  handled = this.errorHandler.handle(err, stage.key);
112
429
  }
113
- errors.push(handled);
430
+ // Унификация: всегда ApiError
431
+ const { toApiError } = await import('./rest-client.js');
432
+ const apiError = toApiError(handled ?? err);
433
+ this.stageResults[key] = { status: 'error', error: apiError };
434
+ this.notifyStageResults();
114
435
  this.progress.updateStage(i, 'error');
436
+ await this.emit(`step:${key}:progress`, 'error');
437
+ // emit step error
438
+ await this.emitStepError({
439
+ stepIndex: i,
440
+ stepKey: key,
441
+ status: 'error',
442
+ error: apiError,
443
+ stageResults: { ...this.stageResults },
444
+ });
115
445
  success = false;
116
446
  break;
117
447
  }
118
448
  }
119
449
 
120
- return { results, errors, success };
450
+ return { stageResults: { ...this.stageResults }, success };
121
451
  }
122
452
  }
@@ -15,6 +15,14 @@ export class ProgressTracker {
15
15
  };
16
16
  }
17
17
 
18
+ /**
19
+ * Возвращает текущий снимок состояния прогресса (не реактивный).
20
+ * Для отслеживания изменений используйте subscribeProgress.
21
+ */
22
+ getProgressRef() {
23
+ return this.progress;
24
+ }
25
+
18
26
  updateStage(stage: number, status: PipelineProgress['stageStatuses'][number]) {
19
27
  this.progress.stageStatuses[stage] = status;
20
28
  this.progress.currentStage = stage;
package/src/types.ts CHANGED
@@ -72,33 +72,81 @@ export type RestRequestConfig = import('axios').AxiosRequestConfig & {
72
72
  skipRateLimit?: boolean;
73
73
  requestId?: string;
74
74
  };
75
- // Типы для конвейерной системы REST API
76
75
 
77
- export type PipelineStageConfig<Input, Output> = {
76
+ /**
77
+ * Конфиг одного шага (этапа) pipeline
78
+ * @template Input Тип входных данных шага
79
+ * @template Output Тип результата шага
80
+ */
81
+ export type PipelineStageConfig<Input = any, Output = any> = {
82
+ /** Уникальный ключ шага */
78
83
  key: string;
79
- request: (input: Input, allResults?: any) => Promise<Output>;
84
+ /** Асинхронная функция-запрос шага */
85
+ request: (input: Input, allResults?: Record<string, PipelineStepResult>) => Promise<Output>;
86
+ /** Условие выполнения шага */
80
87
  condition?: (
81
88
  input: Input,
82
- prevResults: any,
89
+ prevResults: Record<string, PipelineStepResult>,
83
90
  sharedData?: Record<string, any>,
84
91
  ) => boolean;
92
+ /** Количество попыток при ошибке */
85
93
  retryCount?: number;
94
+ /** Таймаут шага (мс) */
86
95
  timeoutMs?: number;
96
+ /** Обработчик ошибок шага */
87
97
  errorHandler?: (error: any, stageKey: string, sharedData?: Record<string, any>) => any;
88
98
  };
89
99
 
100
+
101
+ /**
102
+ * Статус выполнения шага pipeline
103
+ */
104
+ export type PipelineStepStatus = 'pending' | 'loading' | 'success' | 'error' | 'skipped';
105
+
106
+
107
+ /**
108
+ * Результат выполнения шага pipeline
109
+ */
110
+ export type PipelineStepResult = {
111
+ /** Статус шага */
112
+ status: PipelineStepStatus;
113
+ /** Данные результата (если успех) */
114
+ data?: any;
115
+ /** Ошибка (если error) */
116
+ error?: import('./types').ApiError;
117
+ };
118
+
119
+
120
+ /**
121
+ * Конфиг всего pipeline (массив этапов)
122
+ */
90
123
  export type PipelineConfig = {
91
- stages: PipelineStageConfig<any, any>[];
124
+ stages: PipelineStageConfig[];
92
125
  };
93
126
 
127
+
128
+ /**
129
+ * Прогресс выполнения pipeline
130
+ */
94
131
  export type PipelineProgress = {
95
132
  currentStage: number;
96
133
  totalStages: number;
97
- stageStatuses: Array<'pending' | 'in-progress' | 'success' | 'error' | 'skipped'>;
134
+ stageStatuses: Array<PipelineStepStatus>;
98
135
  };
99
136
 
137
+
138
+ /**
139
+ * Результаты всех шагов pipeline (ключ — имя шага)
140
+ */
141
+ export type PipelineStageResults = Record<string, PipelineStepResult>;
142
+
143
+
144
+ /**
145
+ * Итоговый результат выполнения pipeline
146
+ */
100
147
  export type PipelineResult = {
101
- results: any[];
102
- errors: any[];
148
+ /** Результаты по шагам */
149
+ stageResults: PipelineStageResults;
150
+ /** true, если pipeline завершился успешно */
103
151
  success: boolean;
104
152
  };
@@ -0,0 +1,40 @@
1
+ import { ref, onUnmounted } from 'vue';
2
+ import type { PipelineOrchestrator } from '../src/pipeline-orchestrator';
3
+
4
+ /**
5
+ * Vue composition function for subscribing to step events (success/error/progress) for a specific step
6
+ * @param orchestrator PipelineOrchestrator instance
7
+ * @param stepKey string — step key
8
+ * @param eventType 'success' | 'error' | 'progress'
9
+ * @returns Ref<any> — last event payload
10
+ */
11
+ export function usePipelineStepEvent(orchestrator: PipelineOrchestrator, stepKey: string, eventType: 'success' | 'error' | 'progress') {
12
+ const event = ref<any>(null);
13
+ const eventName = `step:${stepKey}:${eventType}`;
14
+ const handler = (payload: any) => { event.value = payload; };
15
+ const unsubscribe = orchestrator.on(eventName, handler);
16
+ onUnmounted(() => unsubscribe && unsubscribe());
17
+ return event;
18
+ }
19
+
20
+ /**
21
+ * Vue composition function for subscribing to pipeline logs
22
+ * @param orchestrator PipelineOrchestrator instance
23
+ * @returns Ref<log[]>
24
+ */
25
+ export function usePipelineLogs(orchestrator: PipelineOrchestrator) {
26
+ const logs = ref(orchestrator.getLogs());
27
+ const handler = () => { logs.value = orchestrator.getLogs(); };
28
+ const unsubscribe = orchestrator.on('log', handler);
29
+ onUnmounted(() => unsubscribe && unsubscribe());
30
+ return logs;
31
+ }
32
+
33
+ /**
34
+ * Vue composition function for rerunning a pipeline step
35
+ * @param orchestrator PipelineOrchestrator instance
36
+ * @returns rerunStep function
37
+ */
38
+ export function useRerunPipelineStep(orchestrator: PipelineOrchestrator) {
39
+ return orchestrator.rerunStep.bind(orchestrator);
40
+ }