Skip to content

Операторы

Операторы — это middleware в use: [], оборачивающее запуск query/mutation. Они складываются как луковица: первый оператор в use — самый внешний. Операторы стадии scheduler (concurrency, debounce) всегда оборачивают операторы стадии executor (retry, timeout, fallback), независимо от порядка в массиве.

ts
query({
  handler,
  use: [
    concurrency({ strategy: "takeLatest" }), // внешний: решает, какой запуск победит
    retry({ times: 3, delay: 300 }),         // внутренний: восстанавливает каждый запуск
  ],
});

concurrency

Стратегия взятия результата — как ведут себя пересекающиеся запуски. Состояние хранится по скоупу и по дорожке, через собственные abort-контроллеры оператора, поэтому новый запуск отменяет только старый (никогда не весь эффект).

СтратегияПоведение
takeLatest (по умолчанию)отменить предыдущий запуск, оставить новейший. Идеально для поиска по мере ввода.
takeFirstпока запуск идёт, новый вызов разделяет его результат (дедуп).
takeEveryбез координации; каждый вызов выполняется независимо.
queueсериализация — новый запуск ждёт завершения текущего.

key: (params) => … разбивает запуски на независимые дорожки (concurrency по id).

retry

Перезапускает хэндлер при ошибке, соблюдая сигнал отмены. Скипы и отмены не ретраятся, а состояние загрузки между попытками остаётся pending.

ts
retry({ times: 3, delay: (attempt) => 2 ** attempt * 100, when: (e) => isNetworkError(e) });

times — максимум ретраев после первой ошибки (по умолчанию 3). delay может быть бэкоффом (attempt, error) => ms. when(error, attempt) отменяет ретрай.

timeout

Прерывает запуск, превысивший ms, и реджектится с TimeoutError. Он гонится с дедлайном, поэтому срабатывает даже если хэндлер игнорирует сигнал — и при этом прерывает контроллер запуска, так что signal-aware хэндлер реально отменяет свой fetch.

ts
use: [timeout(5000)]; // или timeout({ ms: 5000 })

debounce

Ждёт wait мс перед запуском. Сам по себе просто задерживает; в паре с takeLatest становится настоящим debounce — новый запуск прерывает предыдущий, пока тот ещё ждёт.

ts
use: [concurrency({ strategy: "takeLatest" }), debounce({ wait: 300 })];

fallback

Восстанавливает упавший запуск, резолвясь запасным значением вместо ошибки. Скипы и отмены проходят насквозь. Ставьте его перед retry в use, чтобы он ловил только после исчерпания ретраев.

ts
use: [fallback([]), retry({ times: 2 })];                 // статичное значение
use: [fallback((error, params) => cache.get(params.id))]; // вычисляется из error + params

tap

Наблюдает за запуском, не меняя результат — аналитика, логирование, devtools.

ts
use: [
  tap({
    onStart: (params) => log("start", params),
    onSuccess: (data, params) => track("loaded", params),
    onError: (error, params) => report(error),
    onSettled: (params) => {},
  }),
];

Колбэки — сайд-эффекты; чтобы записать стор из колбэка, оберните запись в scoped(ctx.scope, …).

Композиция

use читается снаружи→внутрь. Полезный полный стек:

ts
use: [
  concurrency({ strategy: "takeLatest" }), // scheduler
  debounce({ wait: 300 }),                 // scheduler
  fallback(() => cached),                  // executor (внешний — ловит после retry)
  retry({ times: 3 }),                     // executor
  timeout(8000),                           // executor (внутренний — ограничивает каждую попытку)
];

Новый запуск takeLatest прерывает старый вместе с его циклом ретраев и ожиданием debounce. Поскольку retry/timeout внутренние, все попытки — это один логический запуск для concurrency.

Скипы

Когда оператор отклоняет запуск (дедуп takeFirst, вытесненный takeLatest), он делает это без поддельной ошибки. Вытесненный запуск реджектится с SkipSignal, распознаваемым через isSkip(error) — полезно, когда вы подписаны на failData и хотите игнорировать отмены. Триггеры и refresh глотают их автоматически.

Ещё операторы в планах

cache() (stale-while-revalidate) и гейт barrier запланированы. Другие естественные кандидаты — throttle, poll/keepFresh (перезапрос по интервалу), contract/validate (проверка формы ответа) и circuitBreaker — можно построить на том же контракте Operator.