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 +330 -174
- package/package.json +2 -2
- package/react/usePipelineStepEvents.ts +45 -0
- package/src/pipeline-orchestrator.ts +351 -21
- package/src/progress-tracker.ts +8 -0
- package/src/types.ts +56 -8
- package/vue/usePipelineStepEvents.ts +40 -0
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
|
-
|
|
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
|
-
|
|
30
|
+
|
|
31
|
+
#### Пример: запуск pipeline, обработка ошибок, отслеживание выполнения и использование общего пула данных
|
|
14
32
|
|
|
15
33
|
```js
|
|
16
|
-
|
|
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
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
//
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
const progress = usePipelineProgress(pipeline);
|
|
58
|
-
```
|
|
112
|
+
#### Основные классы и функции
|
|
59
113
|
|
|
60
|
-
|
|
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
|
-
|
|
117
|
+
### createRestClient(config: HttpConfig): RestClient
|
|
118
|
+
Создаёт REST-клиент с поддержкой расширенных возможностей для работы с HTTP API.
|
|
79
119
|
|
|
80
|
-
|
|
120
|
+
#### Пример
|
|
81
121
|
```js
|
|
82
|
-
import {
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
const
|
|
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
|
-
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### RequestExecutor
|
|
147
|
+
Обёртка для выполнения REST-запросов с поддержкой автоматического retry и таймаута.
|
|
148
|
+
|
|
149
|
+
#### Пример
|
|
90
150
|
```js
|
|
91
|
-
import {
|
|
92
|
-
|
|
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
|
-
|
|
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 {
|
|
98
|
-
|
|
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 {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
261
|
+
tracker.updateStage(1, 'success');
|
|
262
|
+
console.log(tracker.getProgress());
|
|
119
263
|
```
|
|
120
264
|
|
|
121
|
-
## Основные API
|
|
122
265
|
|
|
123
|
-
|
|
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
|
-
###
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
176
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
Выполняет GET запрос.
|
|
333
|
+
### Расширение для React
|
|
189
334
|
|
|
190
|
-
####
|
|
191
|
-
Выполняет POST запрос.
|
|
335
|
+
#### Пример: использование в React компоненте
|
|
192
336
|
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
Выполняет DELETE запрос.
|
|
361
|
+
---
|
|
198
362
|
|
|
199
|
-
|
|
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
|
-
|
|
205
|
-
- ✅ Автоматическая обработка JSON
|
|
206
|
-
- ✅ Настраиваемые заголовки
|
|
207
|
-
- ✅ Обработка ошибок
|
|
208
|
-
- ✅ Поддержка таймаутов
|
|
209
|
-
- ✅ Легковесная библиотека без лишних зависимостей
|
|
372
|
+
---
|
|
210
373
|
|
|
211
374
|
## Требования
|
|
212
375
|
|
|
213
|
-
- Node.js >=
|
|
214
|
-
- Современный браузер с поддержкой
|
|
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
|
-
|
|
249
|
-
|
|
402
|
+
GitHub: [macrulezru](https://github.com/macrulezru)
|
|
403
|
+
Сайт: [macrulez.ru](https://macrulez.ru/)
|
|
404
|
+
|
|
405
|
+
---
|
|
250
406
|
|
|
251
407
|
## Поддержка
|
|
252
408
|
|
|
253
|
-
|
|
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
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Pipeline Orchestration Utilities for JavaScript REST API Clients",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
-
"types": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
309
|
+
stageResults: Record<string, import('./types').PipelineStepResult>,
|
|
54
310
|
) => Promise<unknown> | unknown,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
402
|
+
stepResult = await onStepPause(i, stepResult, this.stageResults);
|
|
96
403
|
}
|
|
97
|
-
|
|
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
|
-
|
|
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 {
|
|
450
|
+
return { stageResults: { ...this.stageResults }, success };
|
|
121
451
|
}
|
|
122
452
|
}
|
package/src/progress-tracker.ts
CHANGED
|
@@ -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
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Конфиг одного шага (этапа) pipeline
|
|
78
|
+
* @template Input Тип входных данных шага
|
|
79
|
+
* @template Output Тип результата шага
|
|
80
|
+
*/
|
|
81
|
+
export type PipelineStageConfig<Input = any, Output = any> = {
|
|
82
|
+
/** Уникальный ключ шага */
|
|
78
83
|
key: string;
|
|
79
|
-
|
|
84
|
+
/** Асинхронная функция-запрос шага */
|
|
85
|
+
request: (input: Input, allResults?: Record<string, PipelineStepResult>) => Promise<Output>;
|
|
86
|
+
/** Условие выполнения шага */
|
|
80
87
|
condition?: (
|
|
81
88
|
input: Input,
|
|
82
|
-
prevResults:
|
|
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
|
|
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<
|
|
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
|
-
|
|
102
|
-
|
|
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
|
+
}
|