Operators
Every inline createQuery option is sugar over a standalone operator — import them and apply to any query/mutation (even one built elsewhere). They're composable and tree-shakeable.
import { concurrency, retry, cache, timeout, keepFresh, applyBarrier } from 'effector-refetch';concurrency
How overlapping runs behave: TAKE_LATEST (default), TAKE_FIRST, TAKE_EVERY.
concurrency(searchQuery, { strategy: 'TAKE_LATEST' }); // new run aborts the previousretry
retry(query, 3) or a config. Each attempt is a real effect call; filter decides which failures retry, suppressIntermediateErrors keeps $error clean until the final attempt.
import { exponentialDelay } from 'effector-refetch';
retry(userQuery, {
times: 3,
delay: exponentialDelay(200),
filter: ({ error }) => (error as RequestError).status !== 404, // don't retry 404
});cache
cache(query) (in-memory) or a config (adapter / staleAfter / key / swr / dedupe / purge).
cache(productsQuery, { staleAfter: 30_000, swr: true, purge: loggedOut });timeout
Per-attempt deadline (ms): aborts the in-flight request and fails the run (retryable) if it exceeds it. 0 disables it. Distinct from refetchInterval (poll cadence).
timeout(reportQuery, 5000); // give up a single attempt after 5skeepFresh
Refetch the query with its last params whenever a source store changes or a @@trigger fires — dependency-based freshness (filters, locale, viewer, a write succeeding, a websocket ping). No-op until it has run and while disabled.
keepFresh(productsQuery, { source: $filters }); // or source: [$filters, $locale]
// triggers: anything implementing the @@trigger protocol, or a plain effector Event
keepFresh(productsQuery, { triggers: [createProductMutation, tabFocused] });triggers accepts our own queries/mutations (they implement @@trigger — fired = finished.done), withease web-API triggers, farfetched-compatible triggers, or a raw Event. Each trigger's setup is fired once when wired and stays active for the app's lifetime.
@@trigger protocol
Every query and mutation is a @@trigger: query['@@trigger']() returns { fired, setup, teardown } where fired is finished.done. So a query can drive farfetched's keepFresh({ triggers }) (and vice-versa), or any protocol consumer:
import { keepFresh } from '@farfetched/core';
keepFresh(someFarfetchedQuery, { triggers: [ourQuery] }); // ourQuery succeeds → farfetched refetchesisTrigger(x) narrows to the protocol. Our units are always-on triggers: setup/teardown exist for protocol compatibility but don't gate firing (the query runs on its own scoped lifecycle).
applyBarrier
Gate an already-created query/mutation on a barrier (e.g. 401 → token refresh → resume). Pass null to detach.
const auth = createBarrier({ perform: refreshTokenFx });
applyBarrier(userQuery, auth);Applying an operator more than once
Two well-defined behaviors, by operator kind:
- Last-wins —
concurrency/retry/cache/timeout/applyBarrierare engine setters: a second call replaces the first.retry(q, 1); retry(q, 3)⇒ 3 retries;applyBarrier(q, null)detaches. - Additive —
keepFresh/invalidate/updateadd wiring each call: registering twokeepFreshsources means either change refetches.
This is intentional and tested (test/multi-operators.test.ts) — last-wins for the single-valued config knobs, additive for the ones that register reactions.
All of these equal the corresponding createQuery({ … }) option — use whichever reads better. Runnable: examples/operators.ts.