Операторы
Каждая inline-опция createQuery — это сахар над standalone-оператором. Импортируй и применяй к любому query/mutation (в т.ч. созданному в другом месте). Композируемы и tree-shakeable.
import {
concurrency,
retry,
cache,
timeout,
debounce,
fallback,
keepFresh,
applyBarrier,
} from 'effector-refetch';concurrency
Поведение пересекающихся прогонов: TAKE_LATEST (по умолчанию), TAKE_FIRST, TAKE_EVERY, QUEUE.
concurrency(searchQuery, { strategy: 'TAKE_LATEST' }); // новый прогон отменяет предыдущий
concurrency(saveMutation, { strategy: 'QUEUE' }); // записи идут строго по очередиQUEUE сериализует прогоны: следующий стартует только после завершения предыдущего, ошибки цепочку не рвут, а cancel/reset смывают ожидающих (они абортятся как 'cancelled'). В сочетании с ключом полосы сериализация действует внутри полосы.
Добавьте ключ полосы (lane), чтобы прогоны конкурировали только с прогонами того же ключа — обновление одной строки таблицы больше не отменяет соседние:
concurrency(rowQuery, { strategy: 'TAKE_LATEST', key: ({ rowId }) => String(rowId) });Вытеснение (TAKE_LATEST) и отбрасывание при занятости (TAKE_FIRST) действуют внутри полосы; cancel / reset по-прежнему затрагивают все полосы. $data у query остаётся один — полосы разделяют отмену, а не данные (последний завершившийся прогон записывает стор; данные по ключу храните в кэшируемом query с ключом из params).
Попробуйте вживую — три слота покедекса поверх настоящего PokeAPI, каждый слот — полоса:
Запускаемый скрипт: examples/concurrency-lanes.ts.
retry
retry(query, 3) или конфиг. Каждая попытка — реальный вызов эффекта; filter решает, какие сбои ретраить, suppressIntermediateErrors держит $error чистым до финальной попытки.
import { exponentialDelay } from 'effector-refetch';
retry(userQuery, {
times: 3,
delay: exponentialDelay(200),
filter: ({ error }) => (error as RequestError).status !== 404, // не ретраить 404
});cache
cache(query) (in-memory) или конфиг (adapter / staleAfter / key / swr / dedupe / purge / fillOnAbort).
cache(productsQuery, { staleAfter: 30_000, swr: true, purge: loggedOut });
cache(feedQuery, { swr: { silent: true }, fillOnAbort: true });swr: { silent: true }— упавшая фоновая ревалидация продолжает тихо отдавать stale-запись:$error/$statusне тронуты,finished.failдля наблюдателей всё же срабатывает (иstartAsyncреджектится).fillOnAbort: true— ВЫТЕСНЕННОМУ рану дают долететь, чтобы его ответ всё-таки лёг в кэш; рвётся только его связь с$data/статусом. Явныйcancel/resetабортит по-настоящему.
timeout
Дедлайн одной попытки (мс): прерывает запрос в полёте и роняет прогон (ретраябельно), если превышен. 0 — выкл. Не путать с refetchInterval (частота поллинга).
timeout(reportQuery, 5000); // сдаёмся на одной попытке через 5сdebounce
Подождать N мс перед запуском; более новый запуск той же полосы, стартовавший во время ожидания, вытесняет этот до похода в сеть — честный debounce для поиска по мере ввода под TAKE_LATEST. 0 отключает.
debounce(searchQuery, 300); // или createQuery({ debounce: 300 })fallback
Превратить финальную ошибку (после ретраев) в данные: значение попадает в $data, $status становится done, срабатывает finished.done. Значение не пишется в кэш (это не серверная истина), aborts/skips не затрагиваются. null отключает.
fallback(productsQuery, []); // пустой список вместо экрана ошибки
fallback(profileQuery, ({ error, params }) => cachedProfileOr(params, error));keepFresh
Рефетчит запрос его последними параметрами при изменении стора-source или при срабатывании @@trigger — свежесть по зависимости (фильтры, локаль, пользователь, успешная мутация, ping по websocket). No-op до первого запуска и пока disabled.
keepFresh(productsQuery, { source: $filters }); // или source: [$filters, $locale]
// triggers: что угодно с протоколом @@trigger или обычный effector Event
keepFresh(productsQuery, { triggers: [createProductMutation, tabFocused] });triggers принимает наши же query/mutation (они реализуют @@trigger — fired = finished.done), триггеры web-API из withease, совместимые с farfetched триггеры или сырой Event. У каждого триггера setup дёргается один раз при подключении и остаётся активным на всё время жизни приложения.
Протокол @@trigger
Каждый query и mutation является @@trigger: query['@@trigger']() возвращает { fired, setup, teardown }, где fired — это finished.done. Поэтому запрос можно отдавать в keepFresh({ triggers }) самого farfetched (и наоборот), либо в любого потребителя протокола:
import { keepFresh } from '@farfetched/core';
keepFresh(someFarfetchedQuery, { triggers: [ourQuery] }); // ourQuery успешен → farfetched рефетчитisTrigger(x) сужает тип к протоколу. Наши юниты — always-on триггеры: setup/teardown есть для совместимости с протоколом, но не гейтят срабатывание (запрос живёт своим scoped-жизненным циклом).
applyBarrier
Навешивает на готовый query/mutation barrier (например 401 → обновить токен → продолжить). null — отвязать.
const auth = createBarrier({ perform: refreshTokenFx });
applyBarrier(userQuery, auth);Повторное применение оператора
Два чётко определённых поведения, по типу оператора:
- Last-wins —
concurrency/retry/cache/timeout/debounce/fallback/applyBarrierэто engine-сеттеры: второй вызов заменяет первый.retry(q, 1); retry(q, 3)⇒ 3 ретрая;applyBarrier(q, null)отвязывает. - Аддитивно —
keepFresh/invalidate/updateдобавляют проводку на каждый вызов: два зарегистрированныхkeepFresh-источника — рефетч на изменение любого из них.
Это намеренно и покрыто тестом (test/multi-operators.test.ts) — last-wins для одно-значных конфиг-ручек, аддитивно для тех, что регистрируют реакции.
Всё это эквивалентно соответствующей опции createQuery({ … }) — выбирай, что читается лучше. Рабочий пример: examples/operators.ts.