rest-pipeline-js — пайплайны запросов к API

В приложениях то и дело приходится гонять цепочки запросов: сначала профиль, потом настройки, потом список сущностей — и так далее. Сделать это «на голом» fetch или axios несложно, но как только появляются паузы между шагами, повторные попытки при ошибках, кэш, лимит одновременных запросов и желание показывать пользователю прогресс — код быстро превращается в спагетти. Библиотека rest-pipeline-js как раз про то, чтобы описать такую очередь один раз и не размазывать логику по компонентам.
В этом посте — не про внутреннюю реализацию, а про сценарии: зачем вообще выносить запросы в пайплайн, как устроено ядро и плагины для Vue 3 и React, какие возможности есть у этапов и как этим пользоваться в повседневной работе. Цель — дать полную картину «что можно сделать» с примерами кода под реальные задачи. По ходу разберём и простые кейсы (профиль → настройки), и один более развёрнутый — автоматический подбор авиарейса до готового предложения для шаблона.
Зачем вообще выстраивать очередь запросов
На практике с API часто получается так: один запрос тянет за собой следующий, тот — третий, и уже через пару итераций код обрастает проверками, повторами и разбросанными по компонентам вызовами. Типичные ситуации выглядят так:
- Пошаговая загрузка: нужны данные профиля, потом на их основе — настройки, потом список проектов. Каждый следующий шаг зависит от предыдущего, и хочется не плодить вложенные
thenи размазанные по компонентам вызовы. - Ограничения бэкенда: сервер не выдерживает десяток параллельных запросов — нужна очередь с лимитом одновременных вызовов и при необходимости паузами между шагами, чтобы не ловить 429 или таймауты.
- Единая точка отмены и прогресса: пользователь нажал «Отмена» или ушёл со страницы — все запросы пайплайна должны корректно прерваться; при этом в интерфейсе нужно показывать текущий шаг и статусы («загружаем профиль…», «загружаем настройки…»).
Ручная цепочка с retry, кэшем и метриками быстро обрастает условиями и становится трудночитаемой. rest-pipeline-js даёт единый оркестратор: вы описываете этапы (stages), настраиваете HTTP (baseURL, retry, cache, rate limit), подписываетесь на прогресс и события — запуск, отмена и обновление UI остаются за библиотекой. В итоге логику запросов можно держать в одном месте и при этом не терять контроль над тем, что происходит на каждом шаге. Если позже понадобится добавить новый этап или изменить порядок — правки вносятся в конфиг, а не в несколько компонентов сразу.
Что внутри: ядро и плагины для фреймворков
Чтобы в Vue-проекте не тянуть React, а в «голом» Node или в приложении без фреймворка не тащить лишние зависимости, пакет разделён на три точки входа. Так бандл остаётся предсказуемым: подключаете только то, чем реально пользуетесь.
| Точка входа | Назначение | Содержимое |
|---|---|---|
rest-pipeline-js |
Только ядро | PipelineOrchestrator, createRestClient, типы, утилиты. Без Vue/React. |
rest-pipeline-js/vue |
Vue 3 | Всё из ядра + composition-функции (прогресс, запуск, логи, rerun и т.д.). |
rest-pipeline-js/react |
React | Всё из ядра + хуки (прогресс, запуск, логи, rerun). |
Импорты выглядят так:
- Ядро:
import { PipelineOrchestrator } from "rest-pipeline-js"; - Vue:
import { usePipelineRunVue } from "rest-pipeline-js/vue"; - React:
import { usePipelineRunReact } from "rest-pipeline-js/react";
Ядро (rest-pipeline-js без /vue и `/react**) пригодится, если пайплайн крутится в Node-скрипте, в воркере, по крону или в приложении на другом фреймворке — подписки на прогресс и события делаете вручную через subscribeProgress и on, а данные в UI передаёте как удобно. Подробнее про миграцию с «всё из корня» на отдельные entry points — в посте «Обновление rest-pipeline-js до версии 1.2.5».
Установка — одна команда: npm install rest-pipeline-js. Для React-проекта нужны peer-зависимости react и react-dom — как правило, они уже стоят в проекте, и ничего дополнительно ставить не придётся. Библиотека написана на TypeScript, типы поставляются из коробки; если пишете на чистом JavaScript — импорты и конфиг те же, типы просто не используете.
Как устроен конфиг пайплайна
Всё строится вокруг массива этапов stages: вы перечисляете шаги в нужном порядке, а оркестратор выполняет их по очереди и передаёт результат предыдущего в следующий. Каждый этап — объект с полями: часть из них обязательна (как минимум key и request), остальные подключают нужное поведение — паузы, условия, обработку до и после запроса.
| Поле | Описание |
|---|---|
key |
Уникальный ключ шага (строка). Без него не обойтись. |
request |
Функция (params) => Promise<Output> или возврат URL-строки для GET. |
condition |
Опционально. Если вернёт false, шаг пропускается. |
before |
Хук до запроса: можно подготовить или изменить prev и sharedData. |
after |
Хук после запроса: нормализовать ответ и записать в результат шага. |
errorHandler |
Обработчик ошибки шага (возвращает объект ошибки для сохранения). |
retryCount |
Число повторных попыток при ошибке. |
timeoutMs |
Таймаут шага в миллисекундах. |
pauseBefore |
Пауза (мс) перед выполнением — удобно, когда API не любит частые вызовы. |
pauseAfter |
Пауза (мс) после выполнения. |
Ключ key лучше делать уникальным и осмысленным: по нему потом обращаются к результату в stageResults['profile'] и подписываются на события (step:profile:success). Так и в коде читается, и в логах понятно, какой этап сработал.
В request доступны prev (результат предыдущего шага), allResults (все результаты на текущий момент) и sharedData — общие данные, которые вы передаёте в конструктор оркестратора (токен, идентификатор пользователя и т.п.). Так контекст передаётся между шагами без глобальных переменных, и любой этап может положить во «общий котёл» то, что понадобится дальше.
Ниже — минимальный пример: два шага (профиль и настройки), общие данные в sharedData, а в after первого шага мы дописываем в результат URL запроса — на случай если он пригодится в логах или метриках.
ts
import { PipelineOrchestrator } from "rest-pipeline-js";
type Shared = {
token: string;
requestUrl?: string;
};
const pipelineConfig = {
stages: [
{
key: "profile",
request: async ({ sharedData }: { sharedData: Shared }) => {
sharedData.requestUrl = "/profile";
return sharedData.requestUrl;
},
after: ({ result, sharedData }) => ({
...result,
requestUrl: sharedData.requestUrl,
}),
pauseBefore: 250,
},
{
key: "settings",
request: async ({ sharedData }: { sharedData: Shared }) => {
sharedData.requestUrl = "/settings";
return sharedData.requestUrl;
},
pauseBefore: 250,
},
],
};
const orchestrator = new PipelineOrchestrator({
config: pipelineConfig,
httpConfig: {
baseURL: "https://api.example.com",
timeout: 10_000,
headers: { Authorization: "Bearer TOKEN" },
retry: { attempts: 2, delayMs: 500 },
cache: { enabled: true, ttlMs: 60_000 },
rateLimit: { maxConcurrent: 2 },
},
sharedData: { token: "TOKEN" },
});
Здесь request возвращает строку URL — внутренний исполнитель сам выполняет GET-запрос по этому адресу. Но ничто не мешает в request самому вызвать API (fetch, axios) и вернуть данные: тогда запрос через оркестратор не выполняется, а результат шага — это то, что вернула ваша функция. Так удобно подмешивать нестандартную логику, обращаться к разным доменам или комбинировать несколько вызовов в одном этапе.
Запуск и подписки (ядро)
Чтобы не гадать, на каком шаге пайплайн сейчас и что уже загрузилось — и чтобы, например, показывать пользователю «Загружаем профиль…», «Загружаем настройки…» — можно подписаться на прогресс и на события по шагам. Оркестратор сам обновляет состояние после каждого этапа и вызывает ваши обработчики.
Прогресс — текущий индекс этапа и массив статусов (pending, loading, success, error, skipped). Подписка через subscribeProgress; при каждом изменении вызывается переданный callback.
События — формат step:<key>:success, step:<key>:error, step:<key>:start. Подписка через orchestrator.on(eventName, handler). Удобно для логирования, метрик или точечного обновления UI («шаг profile завершился — обновляем блок настроек»).
Пример: подписываемся и запускаем пайплайн.
ts
// Прогресс: текущий шаг и статусы всех этапов
orchestrator.subscribeProgress((progress) => {
console.log("currentStage:", progress.currentStage);
console.log("stageStatuses:", progress.stageStatuses);
});
// События по шагам
orchestrator.on("step:profile:success", (payload) => {
console.log("profile ok:", payload.data);
});
orchestrator.on("step:settings:error", (payload) => {
console.error("settings failed:", payload.error);
});
// Запуск
const result = await orchestrator.run();
console.log("success:", result.success);
console.log("stageResults:", result.stageResults);
После run() в result окажется итог всего пайплайна: объект с stageResults (результаты по каждому шагу) и флагом success. Дальше можно либо разобрать результаты по ключам этапов, либо передать их в UI — если используете плагины Vue/React, те же данные будут доступны реактивно.
Метод run может принимать опционально:
- onStepPause(stepIndex, stepResult, stageResults) — вызывается после каждого шага; можно вносить паузы или подменять результат перед записью в
stageResults. - externalSignal —
AbortSignalдля отмены извне (например, от кнопки «Отмена»).
Если на каком-то шаге произошла ошибка (и она не была обработана errorHandler так, чтобы продолжить), пайплайн останавливается: success в результате будет false, последующие этапы не выполняются, а в stageResults у этого шага будет статус error и объект ошибки. Так можно сразу понять, на каком этапе «поехало», и показать пользователю осмысленное сообщение или предложить повторить только этот шаг через rerunStep.
Отмена через оркестратор: orchestrator.abort(). Текущий проход по шагам прерывается, подписчики получают ошибку отмены. На практике это важно: пользователь ушёл со страницы или нажал «Отмена» — не нужно вручную отменять каждый запрос и гадать, какой из них ещё висит в воздухе.
Примеры под реальные задачи
Ниже — несколько сценариев от простого к более развёрнутому. Каждый можно взять за основу и подстроить под свой API.
1. Пошаговая загрузка с условием
Задача: загрузить профиль, затем список проектов — но только если в профиле включён флаг «загружать проекты». Иначе шаг с проектами пропускаем, и лишнего запроса не делаем.
Конфиг:
ts
const config = {
stages: [
{
key: "profile",
request: async () => {
const res = await fetch("/api/profile");
return res.json();
},
},
{
key: "projects",
condition: ({ allResults }) => {
const profile = allResults.profile?.data;
return profile?.settings?.loadProjects === true;
},
request: async ({ allResults }) => {
const userId = allResults.profile?.data?.id;
const res = await fetch(`/api/users/${userId}/projects`);
return res.json();
},
},
],
};
Если condition для projects вернёт false, шаг будет помечен как пропущенный и не выполнится. Удобно, когда часть шагов нужна только при определённых настройках или ролях — код остаётся линейным, а ветвление спрятано в одном условии.
2. Подготовка данных и нормализация ответа (before / after)
Задача: перед запросом зафиксировать время в sharedData (например, для кэша или аудита), после ответа — оставить в результате только нужные поля (items и total), чтобы остальной код не зависел от формата API и не таскал лишнее.
Фрагмент этапа: один из элементов массива stages — в реальном конфиге он будет рядом с другими шагами.
ts
{
key: "orders",
before: async ({ prev, sharedData }) => {
sharedData.lastRequestAt = Date.now();
return prev;
},
request: async ({ prev, sharedData }) => {
const res = await api.get("/orders", {
params: { userId: sharedData.userId },
});
return res.data;
},
after: ({ result }) => ({
items: result.items,
total: result.meta.total,
}),
}
Итог шага в stageResults будет таким, какой вернул after. Так нормализацию делаем один раз в пайплайне — и дальше по приложению работаем уже с удобной структурой.
3. Повтор одного шага без полного перезапуска
Задача: пайплайн уже отработал (профиль и настройки загружены). Пользователь нажал «Обновить настройки» — нужно перезапросить только шаг settings, не трогая профиль.
Код:
ts
const updatedStepResult = await orchestrator.rerunStep("settings");
В UI это удобно комбинировать с плагинами Vue или React: кнопка «Обновить настройки» вызывает rerunStep("settings"), подписчики на прогресс и stageResults получают обновление, и интерфейс перерисовывается без полного рестарта пайплайна. Пользователь видит только обновлённый блок, без повторной загрузки всего остального.
4. Автоматический подбор авиарейса для клиента
Задача: по направлению и данным клиента автоматически собрать готовое предложение: получить все рейсы по маршруту → выбрать оптимальный рейс по времени → запросить доступные услуги → выбрать оптимальный набор услуг для этого клиента → получить карту мест → выбрать лучшее свободное место → сформировать итоговое предложение для подстановки в шаблон (письмо, PDF, виджет).
Такой конвейер может жить и на бэкенде (например, в воркере или по крону), и во фронте — когда пользователь один раз задаёт маршрут и хочет получить готовое предложение «в один клик». Каждый шаг опирается на результат предыдущего; логику выбора «оптимального» варианта удобно держать в after, чтобы не смешивать её с самим запросом и при необходимости подменять правила без правок в запросах.
Конфиг пайплайна:
ts
type FlightOfferShared = {
clientId: string;
route: { from: string; to: string; date: string };
// заполняется по ходу пайплайна
selectedFlightId?: string;
selectedServiceIds?: string[];
selectedSeatId?: string;
};
function pickBestFlightByTime(flights: { id: string; departure: string; arrival: string }[]) {
// например: минимум время в пути или удобное время вылета
return flights.sort(
(a, b) => new Date(a.departure).getTime() - new Date(b.departure).getTime()
)[0];
}
function pickBestServicesForClient(
services: { id: string; name: string; price: number }[],
clientId: string,
preferences: Record<string, string[]>
) {
const preferred = preferences[clientId] ?? [];
return services
.filter((s) => preferred.includes(s.id) || preferred.length === 0)
.slice(0, 3);
}
function pickBestFreeSeat(seats: { id: string; row: number; seat: string; free: boolean }[]) {
const free = seats.filter((s) => s.free);
return free.find((s) => s.row <= 10) ?? free[0]; // предпочитаем ряды ближе к выходу
}
const flightOfferConfig = {
stages: [
{
key: "flights",
request: async ({ sharedData }: { sharedData: FlightOfferShared }) => {
const { from, to, date } = sharedData.route;
const res = await api.get("/flights", { params: { from, to, date } });
return res.data;
},
after: ({ result, sharedData }) => {
const best = pickBestFlightByTime(result.flights);
if (best) sharedData.selectedFlightId = best.id;
return { flights: result.flights, selected: best };
},
},
{
key: "services",
condition: ({ allResults }) => !!allResults.flights?.data?.selected,
request: async ({ sharedData }) => {
const flightId = sharedData.selectedFlightId;
const res = await api.get(`/flights/${flightId}/services`);
return res.data;
},
after: ({ result, sharedData }) => {
const clientPrefs = {}; // на практике — из профиля клиента
const selected = pickBestServicesForClient(
result.services,
sharedData.clientId,
clientPrefs
);
sharedData.selectedServiceIds = selected.map((s) => s.id);
return { services: result.services, selected };
},
},
{
key: "seatMap",
condition: ({ allResults }) => !!allResults.services?.data?.selected?.length,
request: async ({ sharedData }) => {
const res = await api.get(`/flights/${sharedData.selectedFlightId}/seats`);
return res.data;
},
after: ({ result, sharedData }) => {
const best = pickBestFreeSeat(result.seats);
if (best) sharedData.selectedSeatId = best.id;
return { seats: result.seats, selected: best };
},
},
{
key: "offer",
request: async ({ allResults, sharedData }) => {
const flights = allResults.flights?.data;
const services = allResults.services?.data;
const seatMap = allResults.seatMap?.data;
return {
flight: flights?.selected,
services: services?.selected ?? [],
seat: seatMap?.selected,
clientId: sharedData.clientId,
route: sharedData.route,
};
},
after: ({ result }) => ({
...result,
templatePayload: {
flightNumber: result.flight?.number,
departure: result.flight?.departure,
arrival: result.flight?.arrival,
seat: result.seat?.seat,
row: result.seat?.row,
services: result.services?.map((s: any) => s.name).join(", "),
},
}),
},
],
};
const orchestrator = new PipelineOrchestrator({
config: flightOfferConfig,
httpConfig: { baseURL: "https://api.avia.example.com", timeout: 15_000 },
sharedData: {
clientId: "client-123",
route: { from: "MOW", to: "LED", date: "2026-04-15" },
},
});
const result = await orchestrator.run();
const offer = result.stageResults.offer?.data;
// offer.templatePayload — готовый объект для шаблона (письмо, PDF, виджет)
Итог: один запуск пайплайна даёт цепочку: рейсы → выбор по времени → услуги → выбор под клиента → карта мест → выбор места → готовый объект для шаблона. Условия condition отсекают шаги, если нет рейсов или не удалось подобрать услуги — не делаем лишних запросов. Логику «оптимальности» (время, предпочтения клиента, ряд в салоне) можно менять в after, не трогая запросы. В итоге конвейер остаётся читаемым, а бизнес-правила сосредоточены в одном месте — удобно и править, и тестировать.
Плагин для Vue 3
Если пайплайн крутится во Vue-приложении, хочется не вручную подписываться на прогресс и результаты, а получить реактивные ref'ы и методы запуска. Во Vue оркестратор создаётся один раз (например, при монтировании страницы или в корне приложения), а реактивность прогресса, результатов и логов обеспечивают composition-функции из rest-pipeline-js/vue. Подписки на оркестратор живут внутри хуков и при размонтировании компонента отписываются сами — за памятью можно не следить.
Доступны:
- usePipelineProgressVue(orchestrator) — ref с объектом прогресса (currentStage, totalStages, stageStatuses).
- usePipelineRunVue(orchestrator) —
run,running,result,error,stageResults,clearStageResults. Запуск пайплайна и всё состояние в одном месте. - usePipelineLogsVue(orchestrator) — ref с массивом логов пайплайна (старты шагов, успехи, ошибки). Удобно для отладочной панели или ленты событий.
- useRerunPipelineStepVue(orchestrator) — функция, которая при вызове с ключом шага выполняет rerunStep(key).
- usePipelineStepEventVue(orchestrator, stepKey, eventType) — ref с последним событием выбранного шага (
success|error|progress). Подходит, когда нужно реагировать именно на завершение одного этапа.
Пример компонента: запуск пайплайна, отображение текущего шага и кнопка повтора второго шага.
vue
<script setup lang="ts">
import { computed } from "vue";
import { PipelineOrchestrator } from "rest-pipeline-js";
import {
usePipelineProgressVue,
usePipelineRunVue,
usePipelineLogsVue,
useRerunPipelineStepVue,
} from "rest-pipeline-js/vue";
const pipelineConfig = {
stages: [
{ key: "step1", request: async () => "/step1" },
{ key: "step2", request: async () => "/step2", pauseBefore: 300 },
],
};
const orchestrator = new PipelineOrchestrator({
config: pipelineConfig,
httpConfig: { baseURL: "https://api.example.com", timeout: 8000 },
});
const progress = usePipelineProgressVue(orchestrator);
const logs = usePipelineLogsVue(orchestrator);
const rerun = useRerunPipelineStepVue(orchestrator);
const { run, running, result, error, stageResults } = usePipelineRunVue(orchestrator);
const currentStage = computed(() => progress.value.currentStage);
</script>
<template>
<div>
<p>Текущий шаг: {{ currentStage }} из {{ progress.totalStages }}</p>
<p>Статусы: {{ progress.stageStatuses?.join(", ") }}</p>
<button :disabled="running" @click="run()">Запустить пайплайн</button>
<button :disabled="running" @click="rerun('step2')">Повторить step2</button>
<pre v-if="error">Ошибка: {{ error?.message }}</pre>
<pre v-if="result">Результат: {{ result }}</pre>
<pre>Результаты по шагам: {{ stageResults }}</pre>
<pre>Логи: {{ logs }}</pre>
</div>
</template>
Один раз создаёте PipelineOrchestrator, передаёте его в хуки — подписки на прогресс и stageResults живут внутри composition-функций и при размонтировании отписываются сами. Так пайплайн остаётся в одном месте, шаги и прогресс видны в шаблоне через обычные ref'ы, а про ручную отписку можно не думать.
Плагин для React
В React та же идея: оркестратор создаётся один раз (лучше через useMemo, чтобы не пересоздавать при каждом рендере), а хуки из rest-pipeline-js/react дают реактивное состояние и функции запуска/rerun. Компонент перерисовывается при изменении прогресса или результатов — достаточно вызвать run() по клику или из эффекта.
Доступны:
- usePipelineProgressReact(orchestrator) — объект прогресса, обновляется при изменении.
- usePipelineRunReact(orchestrator) —
[run, { running, result, error }](в реализации также есть доступ к stageResults через подписку внутри оркестратора). - usePipelineLogsReact(orchestrator) — массив логов.
- useRerunPipelineStepReact(orchestrator) — функция rerunStep(key).
- usePipelineStepEventReact(orchestrator, stepKey, eventType) — последнее событие шага.
Пример компонента:
tsx
import { useMemo } from "react";
import { PipelineOrchestrator } from "rest-pipeline-js";
import {
usePipelineProgressReact,
usePipelineRunReact,
usePipelineLogsReact,
useRerunPipelineStepReact,
} from "rest-pipeline-js/react";
export function DemoPipeline() {
const orchestrator = useMemo(() => {
const pipelineConfig = {
stages: [
{ key: "step1", request: async () => "/step1" },
{ key: "step2", request: async () => "/step2", pauseBefore: 300 },
],
};
return new PipelineOrchestrator({
config: pipelineConfig,
httpConfig: { baseURL: "https://api.example.com", timeout: 8000 },
});
}, []);
const progress = usePipelineProgressReact(orchestrator);
const logs = usePipelineLogsReact(orchestrator);
const rerun = useRerunPipelineStepReact(orchestrator);
const [run, { running, result, error }] = usePipelineRunReact(orchestrator);
return (
<div>
<p>Текущий шаг: {progress.currentStage} из {progress.totalStages}</p>
<p>Статусы: {progress.stageStatuses?.join(", ")}</p>
<button onClick={() => run()} disabled={running}>
Запустить пайплайн
</button>
<button onClick={() => rerun("step2")} disabled={running}>
Повторить step2
</button>
{error && <pre>Ошибка: {error.message}</pre>}
{result && <pre>Результат: {JSON.stringify(result, null, 2)}</pre>}
<pre>Логи: {JSON.stringify(logs, null, 2)}</pre>
</div>
);
}
Важно: оркестратор лучше создавать через useMemo с пустым массивом зависимостей. Иначе при каждом рендере будет создаваться новый экземпляр, подписки внутри хуков будут сбрасываться, и можно поймать лишние запросы или «мигание» прогресса.
HTTP-конфиг: retry, cache, rate limit, metrics
Когда request возвращает URL-строку, запрос выполняет внутренний исполнитель — и как раз для него через httpConfig настраивается общее поведение: базовый адрес, таймауты, заголовки, повторные попытки, кэш, лимит одновременных запросов и метрики. Так не нужно в каждом шаге вручную вызывать retry или проверять кэш — достаточно один раз задать конфиг при создании оркестратора.
| Опция | Описание |
|---|---|
baseURL |
Базовый URL для запросов. |
timeout |
Таймаут запроса (мс). |
headers |
Общие заголовки (например, Authorization). |
retry |
{ attempts, delayMs, backoffMultiplier?, retriableStatus? }. |
cache |
{ enabled, ttlMs } — кэширование ответов. |
rateLimit |
{ maxConcurrent?, maxRequestsPerInterval?, intervalMs? }. |
metrics |
{ onRequestStart, onRequestEnd } — метрики длительности и размера. |
Пример ниже: лимит одновременных запросов (чтобы не перегружать API) и логирование старта/завершения — пригодится и при отладке, и если позже решите считать метрики (длительность, статусы).
ts
httpConfig: {
baseURL: "https://api.example.com",
timeout: 10_000,
retry: { attempts: 2, delayMs: 500 },
rateLimit: { maxConcurrent: 2 },
metrics: {
onRequestStart: (info) => console.log("start", info.url),
onRequestEnd: (info) => console.log("end", info.durationMs, info.status),
},
}
Так можно и снять нагрузку с бэкенда (rate limit), и при необходимости посмотреть, какие запросы уходят и сколько занимают — без правок в каждом шаге.
События и логи
Оркестратор ведёт внутренний лог: старты шагов, успехи, ошибки, отмена. Плюс поддерживается универсальная подписка по имени события — можно реагировать на конкретный шаг или собирать общую ленту. Удобно для отладочной панели («что сейчас выполнилось»), для аналитики или чтобы показывать пользователю пошаговый отчёт.
- Формат событий:
step:<key>:start,step:<key>:success,step:<key>:error,step:<key>:progress. Подписка: orchestrator.on("step:profile:success", handler). - Логи: массив доступен через orchestrator.getLogs(); во Vue и React — через usePipelineLogsVue / usePipelineLogsReact. При каждой записи в лог вызывается событие log — удобно для отладочной панели или вывода в консоль.
Так можно и точечно реагировать на успех или ошибку конкретного шага, и иметь общую ленту событий пайплайна без ручного сбора сообщений по разным подпискам.
Итог: ядро, плагины и сценарии — вместе
rest-pipeline-js закрывает типичные задачи при работе с цепочками REST-запросов: пошаговая загрузка с зависимостями между шагами, паузы и лимиты, retry и таймауты, нормализация данных через before/after, повтор одного шага без полного перезапуска и единая отмена через abort(). Ядро можно использовать в любом окружении (Node, браузер, без фреймворка); плагины для Vue 3 и React подключают реактивный прогресс, запуск, логи и rerun к компонентам без лишней возни с подписками.
Вместе это даёт конвейер: описали этапы → настроили HTTP → подписались на прогресс и события → запускаете пайплайн из UI или из скрипта. Очередь запросов живёт в одном месте, код остаётся читаемым и предсказуемым, а добавлять новые шаги или менять правила выбора (как в примере с авиарейсами) можно без размазывания логики по приложению.
Пакет на npm: rest-pipeline-js