Auth Query Lifecycle Management#
Primary modules: client/session-atom.ts · client/query.ts
This subsystem manages client-side authentication state using nanostores atoms. It handles four interrelated concerns: race condition prevention, duplicate-fetch deduplication, zombie subscription cleanup after unmount-remount cycles, and stable reference preservation to avoid spurious re-renders.
Core Types#
AuthQueryState<T> is the shape of every auth query atom's value:
| Field | Description |
|---|---|
data | Fetched payload, or null |
error | Fetch error (BetterFetchError), or null |
isPending | true until the first data load completes |
isRefetching | true during any in-flight request |
refetch | Stable function reference for manual refetch |
useAuthQuery is the generic atom factory for non-session queries. The session-specific variant is getSessionAtom(), which has a richer deduplication model built on top of useAuthQuery's primitives.
Single Atom-Owned Session Fetch Architecture#
PR #8760 established that the session atom is the sole writer of session state. The refresh manager (session-refresh.ts) acts only as a trigger coordinator, calling fetchSession() but never directly mutating atom state. This eliminates write conflicts between concurrent code paths.
getSessionAtom() initializes the atom with isPending: true and creates a $signal atom for external notifications. Each $signal emission increments sessionRevision and zeroes freshUntil , forcing the next mount to revalidate.
Race Condition Prevention: SessionFlight Deduplication#
fetchSession() tracks in-flight requests via a module-level SessionFlight object :
type SessionFlight = {
cancel: () => void;
promise: Promise<SessionFetchOutcome>;
revision: number;
};
Every call to fetchSession() cancels the current flight (flight?.cancel()) and creates a new AbortController . After settlement, settleFlight() no-ops if flight !== request — preventing stale closures from overwriting state .
fetchSessionOnMount() applies two guards before starting a new fetch:
- In-flight reuse — if
flight.revision === sessionRevision, the mount joins the existing promise instead of starting a new request. This handles React Suspense retries that fire multiple concurrent mounts (PR #10676, PR #10769) . - Freshness window — after a successful fetch,
freshUntilis set tomin(now + SESSION_MOUNT_DEDUPE_INTERVAL, sessionExpiry). Mounts within this window resolve immediately without a network call .SESSION_MOUNT_DEDUPE_INTERVALis aligned to nanostores'STORE_UNMOUNT_DELAY.
Inside executeSessionFetch(), signal.aborted is rechecked after every await point . If needsRefresh is set on the initial GET response, a follow-up POST /get-session is issued on the same signal .
Zombie Atom Cleanup: useAuthQuery Mount Guard#
useAuthQuery's onMount callback manages three flags to prevent zombie subscriptions and duplicate fetches across unmount-remount cycles :
isMounted— settrueon mount,falsein cleanup. Guards post-unmount state writes.isInitialized— local to each mount cycle. The firstinitAtomlistener notification sets this and callsfetchOnMount(); subsequent notifications callfn()directly. AsetTimeout(..., 0)fallback sets it if no listener fires . This is the pattern from PR #9078 that prevents mount-storm fetches.isMountFetchPending/shouldRefetchAfterPending— if a mount fetch is running when a remount occurs, the refetch is queued asshouldRefetchAfterPending = trueand fired after the current fetch settles . PR #10379 introduced this to fix the "permanently stale after one unmount" regression reported in #10363.
The cleanup return sets isMounted = false, cancels the timeout, and removes all initAtom listener subscriptions, preventing stale subscriptions from accumulating.
Stable Reference Preservation & Equality Gating#
Two mechanisms prevent spurious re-renders:
-
Stable data reference — on success, both
useAuthQueryandexecuteSessionFetchuseisJsonEqual()to compare the new response to the current atom value. If deeply equal, the existing object reference is reused. -
Equality-gated atom writes —
withEquality()hooks into nanostores'onSetevent. If the newAuthQueryStateis structurally equivalent to the current value (viaisSessionAtomEqualorisAuthQueryStateEqual), the write is aborted and no listeners fire . ForuseAuthQuery,withEqualityis registered insideonMountso it is tied to the active-subscriber lifetime .
Error Handling Semantics#
| Condition | Behavior |
|---|---|
| HTTP 401 | data → null; session treated as definitively gone |
| Other HTTP errors | Stale data preserved; only error updated |
| Retry in progress | Atom not updated until final retry attempt |
| Non-JSON HTTP 200 | normalizeSessionResponse() accepts any non-{data,error}-shaped response as raw session data without validation |
$sessionSignal and Request Storms#
$sessionSignal ($signal) is a nanostores atom<boolean> that external callers (sign-in, sign-out, user update) toggle to notify the session atom of changes. Each emission increments sessionRevision and zeroes freshUntil, ensuring revalidation on the next mount .
A known issue (#11160) is that rapid $sessionSignal emissions have no damping: each triggers fetchSession(), which cancels the in-flight request and issues a new one, producing a burst of aborted requests. The freshUntil freshness window mitigates this only after the first settled fetch.
Key Source Files#
| File | Role |
|---|---|
client/query.ts | useAuthQuery factory, AuthQueryState type, mount guard, error handling |
client/session-atom.ts | getSessionAtom(), fetchSession(), SessionFlight, freshness dedup |
client/session-refresh.ts | Refresh manager — trigger coordinator for polling and tab-visibility events |
Key PRs#
| PR | Change |
|---|---|
| #8760 | Single atom-owned architecture; AbortController dedup; equality gating |
| #9078 | isInitialized flag to prevent mount-storm fetches |
| #10379 | Restore lifecycle after remount; isMountFetchPending queue |
| #10676 | In-flight Suspense retry deduplication |
| #10769 | Freshness window / freshUntil dedup for settled requests |