session-ready.ts2.8 KBView on GitHub /**
* A one-shot latch that resolves once better-auth has *settled* the question
* "is there a session?" — resolved either way, signed in or not.
*
* Why this exists: nothing gates the authenticated app on session resolution.
* `SessionGuard` was written to (providers/session-provider.tsx) but is not
* mounted anywhere; `AuthMonitor` replaced it and, by design, is a non-rendering
* sibling that redirects from an effect — it cannot hold children back. So the
* whole app shell mounts in one commit and fires its boot fan-out (calendar,
* taskGroups, connections, integrations, crm.*, agentExecutions, settings.get…)
* before the session cookie is established.
*
* In prod that shows up as bursts of ~12 cookie-less requests: every private
* procedure 401s, and `settings.get` — a public procedure — answers 200 with
* all-off defaults, which is how an entitled user gets told "AI is not enabled
* for your account" (see agentAccess.ts).
*
* Gating at the render layer doesn't work: hooks fire their queries whatever the
* component returns, and the firers are spread across providers, the routes
* layout, and child components. The transport is the one place that sees them
* all, so the latch is awaited there (query-provider's fetch handler).
*
* Deliberately NOT a React context: the tRPC fetch handler is plain module code
* with no access to the tree.
*/
/** Hard cap on how long a request will wait for the session to settle. */
const SETTLE_TIMEOUT_MS = 5_000;
let settled = false;
let release: (() => void) | undefined;
let settledPromise: Promise<void>;
function armLatch(): void {
settled = false;
settledPromise = new Promise<void>((resolve) => {
release = resolve;
});
}
armLatch();
/**
* Called once better-auth reports a non-pending session state. Idempotent —
* `useSession` re-renders often, and later calls are no-ops.
*/
export function markSessionSettled(): void {
if (settled) return;
settled = true;
release?.();
}
/** Whether the session question has been answered yet. */
export function isSessionSettled(): boolean {
return settled;
}
/**
* Resolves when the session settles, or after `SETTLE_TIMEOUT_MS`, whichever
* comes first. The timeout matters: if the auth backend is down the session
* never settles, and blocking forever would turn a degraded backend into a
* frozen app. Falling through on timeout restores exactly today's behaviour
* (fire the request and let it 401) rather than adding a new failure mode.
*/
export function whenSessionSettled(): Promise<void> {
if (settled || typeof window === 'undefined') return Promise.resolve();
return Promise.race([
settledPromise,
new Promise<void>((resolve) => setTimeout(resolve, SETTLE_TIMEOUT_MS)),
]);
}
/** Test seam — re-arms the latch between cases. */
export function __resetSessionSettledForTests(): void {
armLatch();
}