Circuit breaker
A barrier is a mutex you can lock and unlock; gated queries wait while it's locked. That's all you need for a circuit breaker: after N consecutive failures, trip the breaker so requests stop hammering a failing backend, wait out a cooldown, then let a trial wave through — a fresh failure re-opens it, a success closes it.
No new API — it's a barrier plus a failure counter.
import { createEffect, createStore, sample } from 'effector';
import { createBarrier, applyBarrier } from 'effector-refetch';
const THRESHOLD = 3; // consecutive failures that trip the breaker
const COOLDOWN = 10_000; // ms the circuit stays "open"
// The "open" window: locking runs the cooldown effect, and the barrier re-opens
// automatically when it settles (createBarrier unlocks on `perform.finally`).
const cooldownFx = createEffect(() => new Promise<void>((r) => setTimeout(r, COOLDOWN)));
const breaker = createBarrier({ perform: cooldownFx });
// Count consecutive failures; any success resets to 0 (→ closed).
const $failures = createStore(0)
.on(api.finished.fail, (n) => n + 1)
.reset(api.finished.done);
// Trip when the threshold is reached. A re-lock while already open is a no-op
// (the store value doesn't change), so the cooldown isn't restarted mid-window.
sample({ clock: $failures.updates, filter: (n) => n >= THRESHOLD, target: breaker.lock });
applyBarrier(api, breaker); // or: createQuery({ effect, barrier: breaker })How it behaves
- Closed —
$failures < THRESHOLD, requests run normally. - Open — the threshold trips
breaker.lock; gated runs pause (theyawaitthe barrier), so a struggling backend stops getting hammered.cooldownFxruns forCOOLDOWN, then the barrier unlocks. - Half-open — the paused request(s) resume. Because
$failuresis still≥ THRESHOLD, a single fresh failure immediately re-trips the breaker (another cooldown); the first success resets the counter and closes it.
breaker.$locked is the "open" flag — bind it with useUnit to show a "service unavailable, retrying in a moment" banner.
Pause vs fail-fast
A textbook breaker fails fast while open; this one pauses requests until the cooldown elapses, then retries them. For a query layer that's usually what you want (no error flash, automatic recovery). If you need fail-fast, sample breaker.$locked into a guard that rejects instead.
Across several queries
Share one breaker over a group — feed it the merged failures and gate each query:
import { merge } from 'effector';
const anyFailure = merge([usersQuery.finished.fail, ordersQuery.finished.fail]);
const $failures = createStore(0)
.on(anyFailure, (n) => n + 1)
.reset([usersQuery.finished.done, ordersQuery.finished.done]);
[usersQuery, ordersQuery].forEach((q) => applyBarrier(q, breaker));Half-open releases every queued request together (fine for the typical one-in-flight-per-query case); for a strict single-trial half-open, add your own gate that lets just one request through after the cooldown. Runnable: examples/circuit-breaker.ts.