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

07.03.2026
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 Copy
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 Copy
// Прогресс: текущий шаг и статусы всех этапов
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.
  • externalSignalAbortSignal для отмены извне (например, от кнопки «Отмена»).

Если на каком-то шаге произошла ошибка (и она не была обработана errorHandler так, чтобы продолжить), пайплайн останавливается: success в результате будет false, последующие этапы не выполняются, а в stageResults у этого шага будет статус error и объект ошибки. Так можно сразу понять, на каком этапе «поехало», и показать пользователю осмысленное сообщение или предложить повторить только этот шаг через rerunStep.

Отмена через оркестратор: orchestrator.abort(). Текущий проход по шагам прерывается, подписчики получают ошибку отмены. На практике это важно: пользователь ушёл со страницы или нажал «Отмена» — не нужно вручную отменять каждый запрос и гадать, какой из них ещё висит в воздухе.


Примеры под реальные задачи

Ниже — несколько сценариев от простого к более развёрнутому. Каждый можно взять за основу и подстроить под свой API.

1. Пошаговая загрузка с условием

Задача: загрузить профиль, затем список проектов — но только если в профиле включён флаг «загружать проекты». Иначе шаг с проектами пропускаем, и лишнего запроса не делаем.

Конфиг:

ts Copy
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 Copy
{
  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 Copy
const updatedStepResult = await orchestrator.rerunStep("settings");

В UI это удобно комбинировать с плагинами Vue или React: кнопка «Обновить настройки» вызывает rerunStep("settings"), подписчики на прогресс и stageResults получают обновление, и интерфейс перерисовывается без полного рестарта пайплайна. Пользователь видит только обновлённый блок, без повторной загрузки всего остального.

4. Автоматический подбор авиарейса для клиента

Задача: по направлению и данным клиента автоматически собрать готовое предложение: получить все рейсы по маршруту → выбрать оптимальный рейс по времени → запросить доступные услуги → выбрать оптимальный набор услуг для этого клиента → получить карту мест → выбрать лучшее свободное место → сформировать итоговое предложение для подстановки в шаблон (письмо, PDF, виджет).

Такой конвейер может жить и на бэкенде (например, в воркере или по крону), и во фронте — когда пользователь один раз задаёт маршрут и хочет получить готовое предложение «в один клик». Каждый шаг опирается на результат предыдущего; логику выбора «оптимального» варианта удобно держать в after, чтобы не смешивать её с самим запросом и при необходимости подменять правила без правок в запросах.

Конфиг пайплайна:

ts Copy
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 Copy
<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 Copy
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 Copy
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

Читать далее

07.03.2026

responsive-media — реактивные брейкпоинты для Vue и не только

Один раз описать брейкпоинты — и во всём приложении использовать реактивные флаги «мобилка / планшет / десктоп» без ручных подписок на matchMedia. Как устроен пакет responsive-media, кастомные брейкпоинты с ориентацией и aspect-ratio и плагин для Vue 3 — в одном посте.

Метки
responsive-mediaVue 3media queriesадаптивная вёрстка
07.03.2026

css-magic-gradient — генерация CSS-градиентов с реактивностью для Vue 3

Пакет css-magic-gradient генерирует линейные, радиальные и конические CSS-градиенты по базовому цвету и опциям, во Vue 3 композиции принимают ref цвета и опций и возвращают вычисляемую строку — при смене темы или выборе цвета градиент обновляется сам, без ручных подписок и watchers.

Метки
css-magic-gradientVue 3CSSградиентыреактивность
07.03.2026

color-value-tools — конвертация и манипуляция цветами в любых форматах

Библиотека color-value-tools — один набор утилит для разбора, конвертации и манипуляции цветом: hex, RGB, HSL, HSV, Lab, LCH, CMYK, яркость, смешивание и контраст по WCAG. В посте — зачем выносить работу с цветом в отдельный слой, обзор API и примеры под реальные задачи (темы, градиенты, доступность, печать).

Метки
color-value-toolsцветаконвертацияTypeScriptдоступность (WCAG)