rest-pipeline-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,259 @@
1
+
2
+ # pipeline-js
3
+
4
+ Модуль для работы с REST API, пайплайнами запросов и отслеживанием прогресса. Не зависит от Vue/React, но легко интегрируется в любые проекты.
5
+
6
+
7
+ ## Установка
8
+
9
+ ### Через NPM (после публикации)
10
+ ```
11
+ npm install pipeline-js
12
+ ```
13
+
14
+ ### Из GitHub (актуально сейчас)
15
+ ```
16
+ npm install git+https://github.com/macrulezru/pipeline-js.git
17
+ ```
18
+
19
+ ## Быстрый старт (чистый JS)
20
+
21
+ ```js
22
+ const { createRestClient, PipelineOrchestrator } = require('pipeline-js');
23
+
24
+ // Создание REST клиента
25
+ const client = createRestClient({ baseURL: 'https://api.example.com' });
26
+ client.get('/endpoint').then(response => {
27
+ console.log(response.data);
28
+ });
29
+
30
+ // Пример пайплайна
31
+ const pipeline = new PipelineOrchestrator({
32
+ stages: [
33
+ {
34
+ key: 'getUser',
35
+ request: async () => client.get('/user'),
36
+ },
37
+ {
38
+ key: 'getPosts',
39
+ request: async (_, results) => client.get(`/posts?userId=${results[0].data.id}`),
40
+ },
41
+ ],
42
+ }, { baseURL: 'https://api.example.com' });
43
+
44
+ // Подписка на прогресс
45
+ const unsubscribe = pipeline.subscribeProgress(progress => {
46
+ console.log('Pipeline progress:', progress);
47
+ });
48
+
49
+ // Получить текущий прогресс (snapshot)
50
+ console.log(pipeline.getProgress());
51
+ ```
52
+
53
+
54
+
55
+ ## React: хуки для интеграции
56
+
57
+ ### usePipelineProgress
58
+ ```jsx
59
+ import { usePipelineProgress } from 'pipeline-js/react';
60
+ import { PipelineOrchestrator } from 'pipeline-js';
61
+
62
+ const pipeline = new PipelineOrchestrator({ stages: [...] }, { baseURL: '...' });
63
+ const progress = usePipelineProgress(pipeline);
64
+ ```
65
+
66
+ ### usePipelineRun
67
+ ```jsx
68
+ import { usePipelineRun } from 'pipeline-js/react';
69
+ const [run, { running, result, error }] = usePipelineRun(pipeline);
70
+
71
+ // В компоненте:
72
+ <button onClick={() => run()}>Старт</button>
73
+ {running && <span>Выполняется...</span>}
74
+ {result && <pre>{JSON.stringify(result)}</pre>}
75
+ {error && <span style={{color:'red'}}>{String(error)}</span>}
76
+ ```
77
+
78
+ ### useRestClient
79
+ ```jsx
80
+ import { useRestClient } from 'pipeline-js/react';
81
+ const api = useRestClient({ baseURL: '...' });
82
+ ```
83
+
84
+ ## Vue: composition-функции для интеграции
85
+
86
+ ### usePipelineProgress
87
+ ```js
88
+ import { usePipelineProgress } from 'pipeline-js/vue';
89
+ import { PipelineOrchestrator } from 'pipeline-js';
90
+
91
+ const pipeline = new PipelineOrchestrator({ stages: [...] }, { baseURL: '...' });
92
+ const progress = usePipelineProgress(pipeline);
93
+ ```
94
+
95
+ ### usePipelineRun
96
+ ```js
97
+ import { usePipelineRun } from 'pipeline-js/vue';
98
+ const { run, running, result, error } = usePipelineRun(pipeline);
99
+ ```
100
+
101
+ ### useRestClient
102
+ ```js
103
+ import { useRestClient } from 'pipeline-js/vue';
104
+ const api = useRestClient({ baseURL: '...' });
105
+ ```
106
+
107
+ ## Использование в Vue 3
108
+
109
+ ```js
110
+ import { createRestClient, PipelineOrchestrator } from 'pipeline-js';
111
+ import { ref, onUnmounted } from 'vue';
112
+
113
+ const client = createRestClient({ baseURL: 'https://api.example.com' });
114
+ const pipeline = new PipelineOrchestrator({
115
+ stages: [
116
+ // ...
117
+ ],
118
+ }, { baseURL: 'https://api.example.com' });
119
+
120
+ const progress = ref(pipeline.getProgress());
121
+ const unsubscribe = pipeline.subscribeProgress(p => {
122
+ progress.value = p;
123
+ });
124
+ onUnmounted(unsubscribe);
125
+ ```
126
+
127
+ ## Основные API
128
+
129
+ ### RestClient
130
+ - `createRestClient(config)` — создать клиента (axios-like API: get, post, request и др.)
131
+
132
+ ### PipelineOrchestrator
133
+ - `new PipelineOrchestrator(pipelineConfig, httpConfig)` — создать пайплайн
134
+ - `subscribeProgress(listener)` — подписка на прогресс, возвращает функцию отписки
135
+ - `getProgress()` — получить текущий прогресс (snapshot)
136
+
137
+ ### ProgressTracker (внутренний)
138
+ - Реактивность реализована через подписки (observer pattern), не зависит от Vue/React
139
+
140
+ ## Структура
141
+ - src/rest-client.ts — основной REST клиент
142
+ - src/types.ts — типы
143
+ - src/request-executor.ts — выполнение запросов
144
+ - src/error-handler.ts — обработка ошибок
145
+ - src/progress-tracker.ts — отслеживание прогресса
146
+ - src/pipeline-orchestrator.ts — оркестрация пайплайна
147
+
148
+ ### POST запрос
149
+
150
+ ```javascript
151
+ api.post('/users', {
152
+ name: 'John Doe',
153
+ email: 'john@example.com'
154
+ })
155
+ .then(response => {
156
+ console.log('User created:', response.data);
157
+ })
158
+ .catch(error => {
159
+ console.error('Error:', error);
160
+ });
161
+ ```
162
+
163
+ ### Использование с async/await
164
+
165
+ ```javascript
166
+ async function getUsers() {
167
+ try {
168
+ const response = await api.get('/users');
169
+ return response.data;
170
+ } catch (error) {
171
+ console.error('Error fetching users:', error);
172
+ throw error;
173
+ }
174
+ }
175
+ ```
176
+
177
+ ## API
178
+
179
+ ### Конструктор
180
+
181
+ ```javascript
182
+ new Pipeline(config)
183
+ ```
184
+
185
+ **Параметры:**
186
+ - `config` (Object) - Объект конфигурации
187
+ - `baseURL` (String) - Базовый URL для всех запросов
188
+ - `timeout` (Number) - Таймаут запроса в миллисекундах
189
+ - `headers` (Object) - Заголовки по умолчанию
190
+
191
+ ### Методы
192
+
193
+ #### `get(url, config)`
194
+ Выполняет GET запрос.
195
+
196
+ #### `post(url, data, config)`
197
+ Выполняет POST запрос.
198
+
199
+ #### `put(url, data, config)`
200
+ Выполняет PUT запрос.
201
+
202
+ #### `delete(url, config)`
203
+ Выполняет DELETE запрос.
204
+
205
+ #### `patch(url, data, config)`
206
+ Выполняет PATCH запрос.
207
+
208
+ ## Особенности
209
+
210
+ - ✅ Поддержка Promise и async/await
211
+ - ✅ Автоматическая обработка JSON
212
+ - ✅ Настраиваемые заголовки
213
+ - ✅ Обработка ошибок
214
+ - ✅ Поддержка таймаутов
215
+ - ✅ Легковесная библиотека без лишних зависимостей
216
+
217
+ ## Требования
218
+
219
+ - Node.js >= 12.0.0
220
+ - Современный браузер с поддержкой ES6
221
+
222
+ ## Разработка
223
+
224
+ ```bash
225
+ # Клонировать репозиторий
226
+ git clone https://github.com/macrulezru/pipeline-js.git
227
+
228
+ # Установить зависимости
229
+ npm install
230
+
231
+ # Запустить тесты
232
+ npm test
233
+
234
+ # Запустить линтер
235
+ npm run lint
236
+ ```
237
+
238
+ ## Вклад в проект
239
+
240
+ Мы приветствуем вклад в развитие проекта! Пожалуйста:
241
+
242
+ 1. Форкните репозиторий
243
+ 2. Создайте ветку для вашей функции (`git checkout -b feature/amazing-feature`)
244
+ 3. Закоммитьте изменения (`git commit -m 'Add some amazing feature'`)
245
+ 4. Запушьте ветку (`git push origin feature/amazing-feature`)
246
+ 5. Откройте Pull Request
247
+
248
+ ## Лицензия
249
+
250
+ MIT
251
+
252
+ ## Автор
253
+
254
+ Git [macrulezru](https://github.com/macrulezru)
255
+ Сайт [macrulez.ru](https://macrulez.ru/)
256
+
257
+ ## Поддержка
258
+
259
+ Если у вас возникли вопросы или проблемы, пожалуйста, создайте [issue](https://github.com/macrulezru/pipeline-js/issues) в репозитории.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "rest-pipeline-js",
3
+ "version": "1.0.0",
4
+ "description": "JS функционал для работы с REST API запросами.",
5
+ "main": "src/rest-client.js",
6
+ "types": "src/types.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "echo \"No tests specified\" && exit 0"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": ""
14
+ },
15
+ "keywords": [
16
+ "rest",
17
+ "api",
18
+ "client",
19
+ "pipeline"
20
+ ],
21
+ "author": "macrulez",
22
+ "license": "MIT",
23
+ "bugs": {
24
+ "url": "https://github.com/macrulezru/pipeline-js"
25
+ },
26
+ "homepage": "https://github.com/macrulezru/pipeline-js#readme",
27
+ "devDependencies": {
28
+ "typescript": "^5.9.3"
29
+ },
30
+ "dependencies": {
31
+ "axios": "^1.13.2"
32
+ }
33
+ }
package/react/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './usePipelineProgress';
2
+ export * from './usePipelineRun';
3
+ export * from './useRestClient';
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { PipelineOrchestrator } from '../src/pipeline-orchestrator';
3
+ import type { PipelineProgress } from '../src/types';
4
+
5
+ /**
6
+ * React hook for subscribing to pipeline progress
7
+ * @param orchestrator PipelineOrchestrator instance
8
+ * @returns PipelineProgress (reactive)
9
+ */
10
+ export function usePipelineProgress(orchestrator: PipelineOrchestrator) {
11
+ const [progress, setProgress] = useState<PipelineProgress>(orchestrator.getProgress());
12
+
13
+ useEffect(() => {
14
+ const unsubscribe = orchestrator.subscribeProgress(setProgress);
15
+ return () => unsubscribe();
16
+ }, [orchestrator]);
17
+
18
+ return progress;
19
+ }
@@ -0,0 +1,33 @@
1
+ import { useCallback, useState } from 'react';
2
+ import type { PipelineOrchestrator } from '../src/pipeline-orchestrator';
3
+ import type { PipelineResult } from '../src/types';
4
+
5
+ /**
6
+ * React hook to run pipeline and track status/result
7
+ * @param orchestrator PipelineOrchestrator instance
8
+ * @returns [run, { running, result, error }]
9
+ */
10
+ export function usePipelineRun(orchestrator: PipelineOrchestrator) {
11
+ const [running, setRunning] = useState(false);
12
+ const [result, setResult] = useState<PipelineResult | null>(null);
13
+ const [error, setError] = useState<any>(null);
14
+
15
+ const run = useCallback(async (...args: any[]) => {
16
+ setRunning(true);
17
+ setError(null);
18
+ setResult(null);
19
+ try {
20
+ // Предполагается, что у orchestrator есть метод run
21
+ const res = await (orchestrator as any).run(...args);
22
+ setResult(res);
23
+ return res;
24
+ } catch (e) {
25
+ setError(e);
26
+ throw e;
27
+ } finally {
28
+ setRunning(false);
29
+ }
30
+ }, [orchestrator]);
31
+
32
+ return [run, { running, result, error }] as const;
33
+ }
@@ -0,0 +1,12 @@
1
+ import { useMemo } from 'react';
2
+ import { createRestClient } from '../src/rest-client';
3
+ import type { HttpConfig } from '../src/types';
4
+
5
+ /**
6
+ * React hook for memoized REST client
7
+ * @param config HttpConfig
8
+ * @returns RestClient instance
9
+ */
10
+ export function useRestClient(config: HttpConfig) {
11
+ return useMemo(() => createRestClient(config), [JSON.stringify(config)]);
12
+ }
package/src/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # Pipeline Framework (src/core/pipeline)
2
+
3
+ ## Описание
4
+
5
+ Механизм pipeline — это универсальная, расширяемая и типобезопасная система для построения цепочек асинхронных операций (пайплайнов) с поддержкой сложных сценариев обработки данных, HTTP-запросов, прогресса, ошибок и метрик.
6
+
7
+ Pipeline позволяет описывать последовательность этапов (stages), каждый из которых может выполнять запрос к API, обработку данных, валидацию, агрегацию и т.д. Каждый этап может зависеть от результатов предыдущих, иметь собственные условия выполнения, обработчики ошибок и параметры таймаута/повторов.
8
+
9
+ ## Основные компоненты
10
+
11
+ - **PipelineOrchestrator** — основной исполнитель пайплайна. Управляет прогрессом, выполняет этапы, обрабатывает ошибки, поддерживает реактивность.
12
+ - **PipelineConfig** — декларативное описание этапов пайплайна (массив stages с ключами, функциями-запросами, условиями, retry, timeout и т.д.).
13
+ - **RequestExecutor** — унифицированный HTTP-клиент с поддержкой retry, таймаутов, метрик, кэширования, rate-limit и отмены запросов.
14
+ - **ProgressTracker** — реактивный трекер прогресса выполнения пайплайна.
15
+ - **ErrorHandler** — централизованная обработка ошибок этапов.
16
+ - **Типы (types.ts)** — строгая типизация всех этапов, конфигов, результатов, ошибок и прогресса.
17
+ - **rest-client.ts** — низкоуровневый HTTP-клиент с интеграцией метрик, кэша, rate-limit и отмены.
18
+
19
+ ## Принципы работы
20
+
21
+ 1. **Декларативность**: pipeline описывается как массив этапов с ключами, функциями-запросами и условиями.
22
+ 2. **Гибкость**: каждый этап может быть асинхронным, иметь свои retry, timeout, обработчик ошибок, условие выполнения.
23
+ 3. **Реактивность**: прогресс пайплайна и результаты доступны как реактивные ссылки (ref/computed).
24
+ 4. **Единый HTTP-стек**: все запросы проходят через RequestExecutor/rest-client, что обеспечивает метрики, кэш, rate-limit, отмену, обработку ошибок и единый стиль логирования.
25
+ 5. **Типобезопасность**: строгая типизация входов/выходов этапов, результатов, ошибок.
26
+ 6. **Интеграция с метриками**: все запросы автоматически логируются в metricsBus и доступны для UI/отладки.
27
+
28
+ ## Преимущества pipeline-механизма
29
+
30
+ - **Единая точка управления логикой и запросами**: все сценарии (от простых до сложных) реализуются через один механизм.
31
+ - **Минимум дублирования**: повторное использование этапов, обработчиков, конфигов.
32
+ - **Простота тестирования**: каждый этап — чистая функция, легко мокается и тестируется.
33
+ - **Гибкая обработка ошибок**: можно задать обработчик на каждый этап или глобально.
34
+ - **Реактивный прогресс**: удобно для UI (progress bar, stepper, отмена, повтор).
35
+ - **Автоматические метрики**: прозрачный сбор статистики по всем запросам и этапам.
36
+ - **Лёгкая интеграция с Vue/Pinia**: pipeline легко встраивается в компоненты и сторы.
37
+ - **Расширяемость**: можно добавлять новые типы этапов, плагины, обработчики, интеграции.
38
+ - **Безопасность и контроль**: строгая типизация, централизованный контроль ошибок и состояния.
39
+
40
+ ## Пример использования
41
+
42
+ ```ts
43
+ import { PipelineOrchestrator } from './pipeline-orchestrator';
44
+ import { RequestExecutor } from './request-executor';
45
+ import type { PipelineConfig } from './types';
46
+
47
+ const httpConfig = { ... };
48
+ const executor = new RequestExecutor(httpConfig);
49
+
50
+ const pipelineConfig: PipelineConfig = {
51
+ stages: [
52
+ {
53
+ key: 'step1',
54
+ request: async () => await executor.execute('endpoint1'),
55
+ retryCount: 2,
56
+ timeoutMs: 10000,
57
+ },
58
+ {
59
+ key: 'step2',
60
+ request: async (prev) => await executor.execute(`endpoint2/${prev.id}`),
61
+ condition: prev => !!prev,
62
+ },
63
+ // ...
64
+ ],
65
+ };
66
+
67
+ const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
68
+ const progress = orchestrator.getProgressRef();
69
+ const result = await orchestrator.run();
70
+ ```
71
+
72
+ ## Рекомендации по использованию
73
+
74
+ - Используйте pipeline для любых цепочек запросов, где важна последовательность, прогресс, обработка ошибок, метрики.
75
+ - Для одиночных запросов используйте RequestExecutor напрямую — это даст метрики и унификацию.
76
+ - Все новые сценарии API/логики реализуйте через pipeline для единообразия и прозрачности.
77
+
78
+ ---
79
+
80
+ **Pipeline — это современный, масштабируемый и прозрачный способ построения сложных сценариев работы с API и асинхронными процессами.**
81
+
82
+ # Pipeline (Конвейерная система) для REST API
83
+
84
+ Модуль для построения декларативных и динамических цепочек HTTP-запросов с передачей данных между этапами, обработкой ошибок, условиями, пользовательскими паузами и полной типобезопасностью.
85
+
86
+ ---
87
+
88
+ ## Архитектура и назначение
89
+
90
+ - **PipelineOrchestrator** — управляет последовательностью этапов, передачей данных, прогрессом и ошибками.
91
+ - **PipelineConfig** — декларативное описание этапов (шагов) pipeline.
92
+ - **PipelineStageConfig** — описание отдельного шага: request, key, condition, retry, timeout, errorHandler.
93
+ - **RequestExecutor** — универсальный исполнитель HTTP-запросов (используется, если не задан кастомный request).
94
+ - **ProgressTracker** — отслеживает статус каждого этапа.
95
+ - **ErrorHandler** — глобальная и локальная обработка ошибок.
96
+
97
+ ---
98
+
99
+ ## Основные возможности
100
+
101
+ - Последовательное выполнение шагов с передачей результата между ними
102
+ - Кастомные request-функции для любого шага (можно реализовать любую логику)
103
+ - Автоматическая или условная передача данных между этапами
104
+ - Условное выполнение этапов (condition)
105
+ - Retry-логика и таймауты на каждом этапе
106
+ - Локальная и глобальная обработка ошибок
107
+ - Пользовательские паузы между шагами (onStepPause)
108
+ - Типобезопасность (TS generics)
109
+ - Использование sharedData для глобального состояния
110
+
111
+ ---
112
+
113
+ ## Пример: Динамический pipeline с передачей данных
114
+
115
+ ````ts
116
+ import { PipelineOrchestrator } from './pipeline-orchestrator';
117
+ import type { PipelineConfig } from './types';
118
+
119
+ # Pipeline (src/core/pipeline)
120
+
121
+ Механизм pipeline — это универсальная, расширяемая и типобезопасная система для построения цепочек асинхронных операций (пайплайнов) с поддержкой сложных сценариев обработки данных, HTTP-запросов, прогресса, ошибок и метрик.
122
+
123
+ ---
124
+
125
+ ## Архитектура и ключевые компоненты
126
+
127
+ - **PipelineOrchestrator** — управляет последовательностью этапов, передачей данных, прогрессом и ошибками.
128
+ - **PipelineConfig** — декларативное описание этапов (шагов) pipeline.
129
+ - **PipelineStageConfig** — описание отдельного шага: request, key, condition, retry, timeout, errorHandler.
130
+ - **RequestExecutor** — универсальный исполнитель HTTP-запросов (используется, если не задан кастомный request).
131
+ - **ProgressTracker** — отслеживает статус каждого этапа, реактивный прогресс для UI.
132
+ - **ErrorHandler** — глобальная и локальная обработка ошибок.
133
+ - **Типы (types.ts)** — строгая типизация всех этапов, конфигов, результатов, ошибок и прогресса.
134
+ - **rest-client.ts** — низкоуровневый HTTP-клиент с интеграцией метрик, кэша, rate-limit и отмены.
135
+
136
+ ---
137
+
138
+ ## Преимущества pipeline-механизма
139
+
140
+ - **Единая точка управления логикой и запросами**: все сценарии (от простых до сложных) реализуются через один механизм.
141
+ - **Минимум дублирования**: повторное использование этапов, обработчиков, конфигов.
142
+ - **Простота тестирования**: каждый этап — чистая функция, легко мокается и тестируется.
143
+ - **Гибкая обработка ошибок**: можно задать обработчик на каждый этап или глобально.
144
+ - **Реактивный прогресс**: удобно для UI (progress bar, stepper, отмена, повтор).
145
+ - **Автоматические метрики**: прозрачный сбор статистики по всем запросам и этапам.
146
+ - **Лёгкая интеграция с Vue/Pinia**: pipeline легко встраивается в компоненты и сторы.
147
+ - **Расширяемость**: можно добавлять новые типы этапов, плагины, обработчики, интеграции.
148
+ - **Безопасность и контроль**: строгая типизация, централизованный контроль ошибок и состояния.
149
+
150
+ ---
151
+
152
+ ## Основные возможности
153
+
154
+ - Последовательное выполнение шагов с передачей результата между ними
155
+ - Кастомные request-функции для любого шага (можно реализовать любую логику)
156
+ - Автоматическая или условная передача данных между этапами
157
+ - Условное выполнение этапов (condition)
158
+ - Retry-логика и таймауты на каждом этапе
159
+ - Локальная и глобальная обработка ошибок
160
+ - Пользовательские паузы между шагами (onStepPause)
161
+ - Типобезопасность (TS generics)
162
+ - Использование sharedData для глобального состояния
163
+
164
+ ---
165
+
166
+ ## Пример: Динамический pipeline с передачей данных
167
+
168
+ ```ts
169
+ import { PipelineOrchestrator } from './pipeline-orchestrator';
170
+ import type { PipelineConfig } from './types';
171
+
172
+ const httpConfig = {
173
+ baseURL: 'https://api.macrulez.ru/v1/fly',
174
+ timeout: 10000,
175
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
176
+ };
177
+
178
+ const pipelineConfig: PipelineConfig = {
179
+ stages: [
180
+ {
181
+ // Первый шаг: получить список точек
182
+ request: async () => {
183
+ const resp = await fetch(`${httpConfig.baseURL}/points`, {
184
+ headers: httpConfig.headers,
185
+ });
186
+ const data = await resp.json();
187
+ const { points } = data;
188
+ if (!Array.isArray(points) || points.length === 0) return null;
189
+ const cityObj = points[Math.floor(Math.random() * points.length)];
190
+ const from = cityObj.point_code;
191
+ const departures = cityObj.departure_to?.split(',').filter(Boolean);
192
+ if (!departures || departures.length === 0) return null;
193
+ const to = departures[Math.floor(Math.random() * departures.length)];
194
+ return { points, from, to };
195
+ },
196
+ },
197
+ {
198
+ // Второй шаг: получить availability по from/to из первого шага
199
+ request: async prev => {
200
+ if (!prev?.from || !prev?.to) return null;
201
+ const url = `${httpConfig.baseURL}/availability/${prev.from}/${prev.to}`;
202
+ const resp = await fetch(url, { headers: httpConfig.headers });
203
+ return await resp.json();
204
+ },
205
+ condition: prev => !!prev?.from && !!prev?.to,
206
+ },
207
+ ],
208
+ };
209
+
210
+ const orchestrator = new PipelineOrchestrator(pipelineConfig, httpConfig);
211
+ orchestrator.run().then(result => {
212
+ console.log('Результаты:', result.results);
213
+ });
214
+ ````
215
+
216
+ ---
217
+
218
+ ## Описание этапа (PipelineStageConfig)
219
+
220
+ - `request: (prevResult) => Promise<result>` — основная функция шага. Получает результат предыдущего шага.
221
+ - `key: string` — (опционально) идентификатор/маршрут для универсального executor (если не используется кастомный request).
222
+ - `condition: (prev, all, shared) => boolean` — (опционально) условие выполнения шага.
223
+ - `retryCount: number` — (опционально) количество повторов при ошибке.
224
+ - `timeoutMs: number` — (опционально) таймаут шага.
225
+ - `errorHandler: (err, key, shared) => any` — (опционально) локальная обработка ошибок.
226
+
227
+ ---
228
+
229
+ ## Как работает orchestrator
230
+
231
+ 1. Идёт по всем stages по порядку.
232
+ 2. Для каждого шага:
233
+ - Если есть condition и оно false — шаг пропускается.
234
+ - Если есть кастомный request — вызывается он (и получает результат предыдущего шага).
235
+ - Если request не задан, но есть key — используется универсальный executor (rest-клиент).
236
+ - Результат шага передаётся в следующий шаг.
237
+ - Ошибки обрабатываются локально (errorHandler) или глобально.
238
+ - Если задан onStepPause — выполнение приостанавливается для пользовательского подтверждения/редактирования результата.
239
+ 3. После завершения возвращает { results, errors, success }.
240
+
241
+ ---
242
+
243
+ ## Best practices
244
+
245
+ - Для динамических шагов всегда используйте кастомный request.
246
+ - key нужен только для универсального executor или для идентификации шага.
247
+ - Для сложных сценариев используйте sharedData для глобального состояния.
248
+ - Всегда обрабатывайте ошибки либо локально (errorHandler), либо глобально.
249
+ - Для UI-интеграции используйте ProgressTracker.
250
+
251
+ ---
252
+
253
+ ## Визуализация прогресса
254
+
255
+ ```ts
256
+ const progress = orchestrator.progress.getProgress();
257
+ // progress.currentStage, progress.stageStatuses
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Зависимости
263
+
264
+ - Использует rest-client.ts для HTTP-запросов (axios)
265
+ - Не требует сторонних библиотек
266
+
267
+ ---
268
+
269
+ **Pipeline — современный, масштабируемый и прозрачный способ построения сложных сценариев работы с API и асинхронными процессами.**
270
+
271
+ **Вопросы и доработки:** Для интеграции с UI или расширения логики — обращайтесь к разработчику модуля.
@@ -0,0 +1,6 @@
1
+ export class ErrorHandler {
2
+ handle(error: any, stageKey: string) {
3
+ // TODO: реализовать классификацию и обработку ошибок
4
+ return { type: 'unknown', error, stageKey };
5
+ }
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Barrel file for pipeline-js module
2
+ export * from './rest-client';
3
+ export * from './types';
4
+ export * from './request-executor';
5
+ export * from './error-handler';
6
+ export * from './progress-tracker';
7
+ export * from './pipeline-orchestrator';
@@ -0,0 +1,122 @@
1
+ import { ErrorHandler } from './error-handler';
2
+ import { ProgressTracker } from './progress-tracker';
3
+ import { RequestExecutor } from './request-executor';
4
+
5
+ import type { PipelineConfig, PipelineResult } from './types';
6
+
7
+ // import { metricsBus } from '@/core/metrics/metrics-bus';
8
+
9
+ export class PipelineOrchestrator {
10
+ private progress: ProgressTracker;
11
+ private errorHandler: ErrorHandler;
12
+ private executor: RequestExecutor;
13
+ private sharedData: Record<string, unknown>;
14
+
15
+ constructor(
16
+ private config: PipelineConfig,
17
+ httpConfig: import('./types').HttpConfig,
18
+ sharedData: Record<string, unknown> = {},
19
+ ) {
20
+ this.progress = new ProgressTracker(config.stages.length);
21
+ this.errorHandler = new ErrorHandler();
22
+ this.executor = new RequestExecutor(httpConfig);
23
+ this.sharedData = sharedData;
24
+ }
25
+
26
+ /**
27
+ * Подписаться на изменения прогресса выполнения pipeline
28
+ * @param listener функция-обработчик изменений
29
+ * @returns функция для отписки
30
+ */
31
+ subscribeProgress(listener: (progress: import('./types').PipelineProgress) => void) {
32
+ return this.progress.subscribe(listener);
33
+ }
34
+
35
+ /**
36
+ * Получить текущий прогресс выполнения pipeline (snapshot, не реактивный)
37
+ */
38
+ getProgress() {
39
+ return this.progress.getProgress();
40
+ }
41
+
42
+ /**
43
+ * @param onStepPause
44
+ * Необязательный callback, вызывается после каждого шага (до перехода к следующему).
45
+ * Позволяет приостановить выполнение, запросить подтверждение пользователя или изменить результат шага.
46
+ * Должен вернуть (optionally изменённый) результат шага или промис с ним.
47
+ * Если не передан — пайплайн работает как раньше.
48
+ */
49
+ async run(
50
+ onStepPause?: (
51
+ stepIndex: number,
52
+ stepResult: unknown,
53
+ results: unknown[],
54
+ ) => Promise<unknown> | unknown,
55
+ ): Promise<PipelineResult> {
56
+ const results: unknown[] = [];
57
+ const errors: unknown[] = [];
58
+ let success = true;
59
+
60
+ for (let i = 0; i < this.config.stages.length; i++) {
61
+ const stage = this.config.stages[i];
62
+ this.progress.updateStage(i, 'in-progress');
63
+
64
+ if (!stage) {
65
+ this.progress.updateStage(i, 'skipped');
66
+ results.push(undefined);
67
+ continue;
68
+ }
69
+
70
+ // Проверка условия выполнения этапа
71
+ if (stage.condition && !stage.condition(results[i - 1], results, this.sharedData)) {
72
+ this.progress.updateStage(i, 'skipped');
73
+ results.push(undefined);
74
+ continue;
75
+ }
76
+ try {
77
+ let stepResult: unknown;
78
+ // Всегда передаём (prev, allResults) в request — best practice для pipeline
79
+ if (typeof stage.request === 'function') {
80
+ stepResult = await stage.request(results[i - 1], results);
81
+ } else if (stage.key) {
82
+ const res = await this.executor.execute(
83
+ stage.key,
84
+ undefined,
85
+ stage.retryCount,
86
+ stage.timeoutMs,
87
+ );
88
+ stepResult = res.data;
89
+ } else {
90
+ stepResult = undefined;
91
+ }
92
+
93
+ // --- Пользовательская пауза/подтверждение/изменение результата ---
94
+ if (onStepPause) {
95
+ stepResult = await onStepPause(i, stepResult, results);
96
+ }
97
+ results.push(stepResult);
98
+ this.progress.updateStage(i, 'success');
99
+
100
+ // ...existing code...
101
+ } catch (err) {
102
+ let handled;
103
+ if (stage && typeof stage.errorHandler === 'function') {
104
+ handled = stage.errorHandler(err, stage.key, this.sharedData);
105
+ } else if (stage) {
106
+ handled = this.errorHandler.handle(err, stage.key);
107
+ } else {
108
+ handled = this.errorHandler.handle(err, 'unknown');
109
+ }
110
+ if (!handled && stage) {
111
+ handled = this.errorHandler.handle(err, stage.key);
112
+ }
113
+ errors.push(handled);
114
+ this.progress.updateStage(i, 'error');
115
+ success = false;
116
+ break;
117
+ }
118
+ }
119
+
120
+ return { results, errors, success };
121
+ }
122
+ }
@@ -0,0 +1,42 @@
1
+
2
+ import type { PipelineProgress } from './types';
3
+
4
+ type ProgressListener = (progress: PipelineProgress) => void;
5
+
6
+ export class ProgressTracker {
7
+ private progress: PipelineProgress;
8
+ private listeners: ProgressListener[] = [];
9
+
10
+ constructor(totalStages: number) {
11
+ this.progress = {
12
+ currentStage: 0,
13
+ totalStages,
14
+ stageStatuses: Array(totalStages).fill('pending'),
15
+ };
16
+ }
17
+
18
+ updateStage(stage: number, status: PipelineProgress['stageStatuses'][number]) {
19
+ this.progress.stageStatuses[stage] = status;
20
+ this.progress.currentStage = stage;
21
+ this.notify();
22
+ }
23
+
24
+ getProgress() {
25
+ return { ...this.progress };
26
+ }
27
+
28
+ subscribe(listener: ProgressListener) {
29
+ this.listeners.push(listener);
30
+ // Немедленно уведомляем нового подписчика о текущем состоянии
31
+ listener({ ...this.progress });
32
+ return () => {
33
+ this.listeners = this.listeners.filter(l => l !== listener);
34
+ };
35
+ }
36
+
37
+ private notify() {
38
+ for (const listener of this.listeners) {
39
+ listener({ ...this.progress });
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ import { getRestClient } from './rest-client';
2
+
3
+ import type { RestRequestConfig, HttpConfig, ApiResponse } from './types';
4
+
5
+ export class RequestExecutor {
6
+ private client;
7
+
8
+ constructor(httpConfig: HttpConfig) {
9
+ this.client = getRestClient(httpConfig);
10
+ }
11
+
12
+ /**
13
+ * Выполнение одного запроса с поддержкой retry и таймаута
14
+ */
15
+ async execute<T = any>(
16
+ command: string,
17
+ reqConfig?: RestRequestConfig,
18
+ retryCount = 0,
19
+ timeoutMs = 10000,
20
+ ): Promise<ApiResponse<T>> {
21
+ let attempt = 0;
22
+ let lastError: any = null;
23
+ while (attempt <= retryCount) {
24
+ try {
25
+ const result = await Promise.race([
26
+ this.client.request<T>(command, reqConfig),
27
+ new Promise((_, reject) =>
28
+ setTimeout(() => reject(new Error('Timeout')), timeoutMs),
29
+ ),
30
+ ]);
31
+ return result as ApiResponse<T>;
32
+ } catch (err) {
33
+ lastError = err;
34
+ attempt++;
35
+ if (attempt > retryCount) throw lastError;
36
+ }
37
+ }
38
+ throw lastError;
39
+ }
40
+ }
@@ -0,0 +1,178 @@
1
+ import axios from 'axios';
2
+
3
+ import type { HttpConfig, ApiError, ApiResponse, RestRequestConfig } from './types';
4
+ import type { AxiosInstance, AxiosResponse } from 'axios';
5
+
6
+ type RestClient = ReturnType<typeof createRestClient>;
7
+
8
+ export function toApiError(error: unknown): ApiError {
9
+ if (axios.isCancel(error)) {
10
+ return {
11
+ message: 'Запрос был отменен',
12
+ code: 'REQUEST_CANCELLED',
13
+ };
14
+ }
15
+ if (axios.isAxiosError(error)) {
16
+ const axiosError = error;
17
+ return {
18
+ message: axiosError.message,
19
+ code: axiosError.code,
20
+ status: axiosError.response?.status,
21
+ timestamp: new Date(),
22
+ };
23
+ }
24
+ if (error instanceof Error) {
25
+ return {
26
+ message: error.message,
27
+ timestamp: new Date(),
28
+ };
29
+ }
30
+ return {
31
+ message: 'Произошла неизвестная ошибка',
32
+ timestamp: new Date(),
33
+ };
34
+ }
35
+
36
+ const restClientCache: Map<string, RestClient> = new Map();
37
+
38
+ export function createRestClient(config: HttpConfig) {
39
+ const httpClient: AxiosInstance = axios.create({
40
+ baseURL: config.baseURL,
41
+ timeout: config.timeout,
42
+ headers: config.headers,
43
+ withCredentials: config.withCredentials,
44
+ });
45
+ // ...реализация rate limit, cache, interceptors, request, etc. (скопировать из rest.ts при необходимости)
46
+ // Для краткости: реализуйте полный функционал по мере необходимости.
47
+ async function request<T = unknown>(
48
+ command: string,
49
+ req?: RestRequestConfig,
50
+ ): Promise<ApiResponse<T>> {
51
+ const reqId = req?.requestId ?? Math.random().toString(36).slice(2);
52
+ const methodUpper = (req?.method ?? 'GET').toUpperCase();
53
+ const fullUrl = `${config.baseURL}${command}`;
54
+ config.metrics?.onRequestStart?.({
55
+ id: reqId,
56
+ method: methodUpper,
57
+ url: fullUrl,
58
+ timestamp: Date.now(),
59
+ requestBody: req?.data,
60
+ requestParams: req?.params,
61
+ requestHeaders: req?.headers as Record<string, string>,
62
+ });
63
+ const startTs = Date.now();
64
+ try {
65
+ const response: AxiosResponse<T> = await httpClient.request<T>({
66
+ url: command,
67
+ ...req,
68
+ });
69
+ const payload: ApiResponse<T> = {
70
+ data: response.data,
71
+ status: response.status,
72
+ statusText: response.statusText,
73
+ headers: response.headers as Record<string, string>,
74
+ };
75
+ const duration = Date.now() - startTs;
76
+ // --- вычисление размера ответа ---
77
+ let responseBytes: number | undefined = undefined;
78
+ const headers = response.headers as Record<string, string>;
79
+ const contentLengthHeader =
80
+ headers['content-length'] || headers['Content-Length'] || undefined;
81
+ const parsedLength = contentLengthHeader ? Number(contentLengthHeader) : undefined;
82
+ if (Number.isFinite(parsedLength) && parsedLength !== 0) {
83
+ responseBytes = parsedLength;
84
+ } else {
85
+ try {
86
+ const raw = response.data;
87
+ if (typeof raw === 'string') {
88
+ responseBytes = new TextEncoder().encode(raw).length;
89
+ } else if (raw !== undefined) {
90
+ const str = JSON.stringify(raw);
91
+ responseBytes = new TextEncoder().encode(str).length;
92
+ }
93
+ } catch {
94
+ // ignore sizing errors
95
+ }
96
+ }
97
+ config.metrics?.onRequestEnd?.({
98
+ id: reqId,
99
+ durationMs: duration,
100
+ status: response.status,
101
+ bytes: responseBytes,
102
+ responseBody: response.data,
103
+ responseHeaders: response.headers as Record<string, string>,
104
+ });
105
+ return payload;
106
+ } catch (error) {
107
+ const duration = Date.now() - startTs;
108
+ config.metrics?.onRequestEnd?.({
109
+ id: reqId,
110
+ durationMs: duration,
111
+ error: toApiError(error),
112
+ });
113
+ throw error;
114
+ }
115
+ }
116
+ // --- Реализация cancellableRequest ---
117
+ const cancelTokenSources: Map<string, any> = new Map();
118
+ function cancelRequest(key: string): void {
119
+ const source = cancelTokenSources.get(key);
120
+ if (source) {
121
+ source.cancel(`Запрос отменен по ключу: ${key}`);
122
+ cancelTokenSources.delete(key);
123
+ }
124
+ }
125
+ async function cancellableRequest<T = unknown>(
126
+ key: string,
127
+ command: string,
128
+ config?: any,
129
+ ): Promise<ApiResponse<T>> {
130
+ cancelRequest(key);
131
+ const axios = await import('axios');
132
+ const source = axios.default.CancelToken.source();
133
+ cancelTokenSources.set(key, source);
134
+ try {
135
+ return await request<T>(command, {
136
+ ...config,
137
+ cancelToken: source.token,
138
+ });
139
+ } finally {
140
+ cancelTokenSources.delete(key);
141
+ }
142
+ }
143
+ return {
144
+ request,
145
+ get: <T = unknown>(command: string, config?: Omit<RestRequestConfig, 'method'>) =>
146
+ request<T>(command, { ...config, method: 'GET' }),
147
+ post: <T = unknown>(
148
+ command: string,
149
+ data?: unknown,
150
+ config?: Omit<RestRequestConfig, 'method' | 'data'>,
151
+ ) => request<T>(command, { ...config, method: 'POST', data }),
152
+ put: <T = unknown>(
153
+ command: string,
154
+ data?: unknown,
155
+ config?: Omit<RestRequestConfig, 'method' | 'data'>,
156
+ ) => request<T>(command, { ...config, method: 'PUT', data }),
157
+ delete: <T = unknown>(command: string, config?: Omit<RestRequestConfig, 'method'>) =>
158
+ request<T>(command, { ...config, method: 'DELETE' }),
159
+ cancellableRequest,
160
+ cancelRequest,
161
+ };
162
+ }
163
+
164
+ export function getRestClient(config: HttpConfig): RestClient {
165
+ const key = JSON.stringify({
166
+ baseURL: config.baseURL,
167
+ timeout: config.timeout,
168
+ withCredentials: config.withCredentials,
169
+ headers: config.headers ?? {},
170
+ retry: config.retry ?? {},
171
+ metrics: !!config.metrics,
172
+ });
173
+ const cachedClient = restClientCache.get(key);
174
+ if (cachedClient) return cachedClient;
175
+ const client = createRestClient(config);
176
+ restClientCache.set(key, client);
177
+ return client;
178
+ }
package/src/types.ts ADDED
@@ -0,0 +1,104 @@
1
+ // --- Типы для HTTP и REST ---
2
+ export interface RetryConfig {
3
+ attempts: number;
4
+ delayMs: number;
5
+ backoffMultiplier: number;
6
+ retriableStatus?: number[];
7
+ }
8
+
9
+ export type RetryOptions = Partial<RetryConfig>;
10
+
11
+ export interface CacheConfig {
12
+ enabled: boolean;
13
+ ttlMs: number;
14
+ }
15
+
16
+ export interface RateLimitConfig {
17
+ maxConcurrent?: number;
18
+ maxRequestsPerInterval?: number;
19
+ intervalMs?: number;
20
+ }
21
+
22
+ export interface MetricsHandler {
23
+ onRequestStart?: (info: {
24
+ id: string;
25
+ method?: string;
26
+ url?: string;
27
+ timestamp: number;
28
+ requestBody?: unknown;
29
+ requestParams?: unknown;
30
+ requestHeaders?: Record<string, string>;
31
+ }) => void;
32
+ onRequestEnd?: (info: {
33
+ id: string;
34
+ durationMs: number;
35
+ status?: number;
36
+ error?: ApiError;
37
+ bytes?: number;
38
+ responseBody?: unknown;
39
+ responseHeaders?: Record<string, string>;
40
+ }) => void;
41
+ }
42
+
43
+ export interface HttpConfig {
44
+ baseURL: string;
45
+ timeout?: number;
46
+ headers?: Record<string, string>;
47
+ withCredentials?: boolean;
48
+ retry?: RetryOptions;
49
+ cache?: CacheConfig;
50
+ rateLimit?: RateLimitConfig;
51
+ metrics?: MetricsHandler;
52
+ }
53
+
54
+ export interface ApiError {
55
+ message: string;
56
+ code?: string | number;
57
+ status?: number;
58
+ timestamp?: Date;
59
+ }
60
+
61
+ export interface ApiResponse<T = unknown> {
62
+ data: T;
63
+ status: number;
64
+ statusText: string;
65
+ headers: Record<string, string>;
66
+ }
67
+
68
+ export type RestRequestConfig = import('axios').AxiosRequestConfig & {
69
+ useCache?: boolean;
70
+ cacheTtlMs?: number;
71
+ cacheKey?: string;
72
+ skipRateLimit?: boolean;
73
+ requestId?: string;
74
+ };
75
+ // Типы для конвейерной системы REST API
76
+
77
+ export type PipelineStageConfig<Input, Output> = {
78
+ key: string;
79
+ request: (input: Input, allResults?: any) => Promise<Output>;
80
+ condition?: (
81
+ input: Input,
82
+ prevResults: any,
83
+ sharedData?: Record<string, any>,
84
+ ) => boolean;
85
+ retryCount?: number;
86
+ timeoutMs?: number;
87
+ errorHandler?: (error: any, stageKey: string, sharedData?: Record<string, any>) => any;
88
+ };
89
+
90
+ export type PipelineConfig = {
91
+ stages: PipelineStageConfig<any, any>[];
92
+ };
93
+
94
+ export type PipelineProgress = {
95
+ currentStage: number;
96
+ totalStages: number;
97
+ stageStatuses: Array<'pending' | 'in-progress' | 'success' | 'error' | 'skipped'>;
98
+ };
99
+
100
+ export type PipelineResult = {
101
+ results: any[];
102
+ errors: any[];
103
+ success: boolean;
104
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ // TypeScript configuration for building the module
2
+ {
3
+ "compilerOptions": {
4
+ "target": "ES2019",
5
+ "module": "commonjs",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }
package/vue/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './usePipelineProgress';
2
+ export * from './usePipelineRun';
3
+ export * from './useRestClient';
@@ -0,0 +1,17 @@
1
+ import { ref, onUnmounted } from 'vue';
2
+ import type { PipelineOrchestrator } from '../src/pipeline-orchestrator';
3
+ import type { PipelineProgress } from '../src/types';
4
+
5
+ /**
6
+ * Vue composition function for subscribing to pipeline progress
7
+ * @param orchestrator PipelineOrchestrator instance
8
+ * @returns Ref<PipelineProgress>
9
+ */
10
+ export function usePipelineProgress(orchestrator: PipelineOrchestrator) {
11
+ const progress = ref<PipelineProgress>(orchestrator.getProgress());
12
+ const unsubscribe = orchestrator.subscribeProgress(p => {
13
+ progress.value = p;
14
+ });
15
+ onUnmounted(unsubscribe);
16
+ return progress;
17
+ }
@@ -0,0 +1,33 @@
1
+ import { ref } from 'vue';
2
+ import type { PipelineOrchestrator } from '../src/pipeline-orchestrator';
3
+ import type { PipelineResult } from '../src/types';
4
+
5
+ /**
6
+ * Vue composition function to run pipeline and track status/result
7
+ * @param orchestrator PipelineOrchestrator instance
8
+ * @returns { run, running, result, error }
9
+ */
10
+ export function usePipelineRun(orchestrator: PipelineOrchestrator) {
11
+ const running = ref(false);
12
+ const result = ref<PipelineResult | null>(null);
13
+ const error = ref<any>(null);
14
+
15
+ async function run(...args: any[]) {
16
+ running.value = true;
17
+ error.value = null;
18
+ result.value = null;
19
+ try {
20
+ // Предполагается, что у orchestrator есть метод run
21
+ const res = await (orchestrator as any).run(...args);
22
+ result.value = res;
23
+ return res;
24
+ } catch (e) {
25
+ error.value = e;
26
+ throw e;
27
+ } finally {
28
+ running.value = false;
29
+ }
30
+ }
31
+
32
+ return { run, running, result, error };
33
+ }
@@ -0,0 +1,12 @@
1
+ import { computed } from 'vue';
2
+ import { createRestClient } from '../src/rest-client';
3
+ import type { HttpConfig } from '../src/types';
4
+
5
+ /**
6
+ * Vue composition function for memoized REST client
7
+ * @param config HttpConfig
8
+ * @returns RestClient instance
9
+ */
10
+ export function useRestClient(config: HttpConfig) {
11
+ return computed(() => createRestClient(config));
12
+ }