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();
}