Авторизация и barrier (пауза окружения)
Иногда нужно поставить на паузу все запросы, что-то сделать и продолжить — классический случай 401: пауза, рефреш токена, доигрывание очереди запросов.
createBarrier — это мьютекс, на котором запросы ждут. Пока он заблокирован, любой gated-запрос, который пытается выполниться, блокируется; при разблокировке очередь продолжается.
import { sample } from 'effector';
import { createBarrier, createQueryFactory } from 'effector-refetch';
// barrier запускает рефреш при блокировке и разблокируется, когда рефреш завершится
const authBarrier = createBarrier({ perform: refreshTokenFx });
// все query/мутации этой фабрики ждут на barrier
const { createQuery, createMutation } = createQueryFactory({ barrier: authBarrier });
const profile = createQuery({
effect: getProfileFx, // бросает { status: 401 } при протухшем токене
retry: { times: 1, filter: ({ error }) => error.status === 401 },
});
// при 401 — блокируем barrier, что запускает refreshTokenFx
sample({
clock: getProfileFx.failData,
filter: (error) => error.status === 401,
target: authBarrier.lock,
});Что происходит при протухшем токене:
getProfileFxпадает с401→ barrier блокируется, запускаетсяrefreshTokenFx.retryпланирует повтор — но он ждёт на barrier.- Другие запросы, стартовавшие тем временем, тоже встают в очередь.
refreshTokenFxзавершается → barrier разблокируется → повтор (и очередь) выполняются со свежим токеном.
Попробуйте вживую
Протухните токен и нажмите Fetch ×3: один 401 блокирует barrier, рефреш выполняется один раз, а все повторные запросы после разблокировки продолжаются и успешно завершаются:
API
const barrier = createBarrier({ perform?: Effect<void, any> });
barrier.lock(); // закрыть — gated-запросы ждут
barrier.unlock(); // открыть — очередь продолжается
barrier.$locked; // Store<boolean>С perform блокировка автоматически запускает эффект и разблокируется по его завершении (успех или ошибка — без дедлока). Без него — управляйте lock/unlock сами.
Gate одного запроса без фабрики — через опцию конфига или оператор applyBarrier на уже созданном query/mutation (null — отвязать):
const q = createQuery({ effect: fx, barrier: authBarrier });
// или после создания:
applyBarrier(existingQuery, authBarrier);Лочить надо на самой ошибке, а не на finished.fail
finished.fail срабатывает только на финальной ошибке — когда ретраи уже исчерпаны. Блокировка по нему опаздывает: ретрай уже ушёл с протухшим токеном. Вешайте lock на сырой эффект (fx.failData) — он стреляет на самом первом 401:
// ✅ срабатывает на первом 401, до планирования ретрая
sample({ clock: getProfileFx.failData, filter: (e) => e.status === 401, target: authBarrier.lock });
// ❌ только после того, как все ретраи уже провалились
sample({
clock: profile.finished.fail,
filter: ({ error }) => error.status === 401,
target: authBarrier.lock,
});HTTP-слой подходит не хуже — лочьте там, где увидели 401, перед пробросом ошибки. Этот код выполняется вне стека вызовов effector, поэтому привяжите вызов к scope:
import { scopeBind } from 'effector';
async function request(url: string) {
const response = await fetch(url);
if (response.status === 401) {
scopeBind(authBarrier.lock, { safe: true })();
throw Object.assign(new Error('Unauthorized'), { status: 401 });
}
return response.json();
}Бесконечные запросы
createInfiniteQuery принимает barrier и retry, поэтому постраничные ленты получают тот же сценарий: start, fetchNext и fetchPrevious ждут, пока барьер закрыт, и повторная попытка загрузки страницы ждёт тоже — именно она переигрывает упавшую на 401 страницу после обновления токена.
const feed = createInfiniteQuery({
effect: fetchPageFx,
initialPageParam: 0,
getNextPageParam: ({ lastPage }) => lastPage.next,
barrier: authBarrier,
retry: { times: 1, filter: ({ error }) => error.status === 401 },
});refetchAll перезагружает окно напрямую через эффект, мимо page-запроса, поэтому попытки выполняет сам цикл перезагрузки — по тому же конфигу retry и с ожиданием барьера перед каждой попыткой. Поэтому 401 в середине окна ведёт себя как везде: рефреш выполняется, страница переигрывается, перезагрузка доходит до конца. Перезагрузка падает, только когда у страницы кончились попытки (или retry.filter отверг ошибку), — тогда на экране остаётся прежнее окно, а ошибка попадает в $error / $status.
Загрузите страницу-другую, протухните токен и подгрузите следующую: 401 блокирует барьер, рефреш выполняется один раз, а повтор страницы продолжается после разблокировки:
Запускаемая версия того же сценария (фейковый API, без сети) — в examples/infinite-auth-barrier.ts: npx tsx examples/infinite-auth-barrier.ts.
Scope и SSR
Барьер fork-безопасен: и флаг блокировки, и очередь ожидающих запусков лежат в сторах, поэтому параллельные scope — SSR-запросы, тесты — блокируются и освобождаются независимо. Блокировка одного scope не мешает остальным.
Блокировка извне effector
lock(), вызванный из HTTP-слоя, колбэка SDK или любого другого места вне стека вызовов effector, попадёт в бесскоупное приложение, и scope-запросы его не увидят. Оборачивайте в scopeBind(barrier.lock, { safe: true }) (как выше) или лочьте декларативно через sample.