UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

168 lines (167 loc) • 4.98 kB
import { isJsonEqual, withEquality } from "./equality.mjs"; import { createSessionRefreshManager } from "./session-refresh.mjs"; import { STORE_UNMOUNT_DELAY, atom, onMount } from "nanostores"; //#region src/client/session-atom.ts const isServer = () => typeof window === "undefined"; const SESSION_MOUNT_DEDUPE_INTERVAL = STORE_UNMOUNT_DELAY; /** * Normalize $fetch response: `throw: true` returns data directly, * otherwise `{ data, error }`. */ function normalizeSessionResponse(res) { if (typeof res === "object" && res !== null && "data" in res && "error" in res) return res; return { data: res, error: null }; } function normalizeSessionData(data) { if (!data) return null; if (data.session === null && data.user === null) return null; return data; } function isSessionAtomEqual(a, b) { return isJsonEqual(a.data, b.data) && a.error === b.error && a.isPending === b.isPending && a.isRefetching === b.isRefetching && a.refetch === b.refetch; } function getSessionAtom($fetch, options) { const $signal = atom(false); let flight; let freshUntil = 0; let sessionRevision = 0; $signal.listen(() => { sessionRevision++; freshUntil = 0; }); const refetch = (queryParams) => fetchSession(queryParams); const session = atom({ data: null, error: null, isPending: true, isRefetching: false, refetch }); withEquality(session, isSessionAtomEqual); const executeSessionFetch = async (signal, queryParams) => { const current = session.value; session.set({ ...current, isPending: current.data === null, isRefetching: true, error: null, refetch }); if (signal.aborted) return "aborted"; try { const res = await $fetch("/get-session", { method: "GET", query: queryParams?.query, signal }); if (signal.aborted) return "aborted"; let { data, error } = normalizeSessionResponse(res); let outcome = "fresh"; if (data?.needsRefresh) try { const refreshRes = await $fetch("/get-session", { method: "POST", signal }); if (signal.aborted) return "aborted"; ({data, error} = normalizeSessionResponse(refreshRes)); } catch { if (signal.aborted) return "aborted"; outcome = "stale"; } if (error) { const latest = session.value; const isUnauthorized = error?.status === 401; session.set({ data: isUnauthorized ? null : latest.data, error, isPending: false, isRefetching: false, refetch }); return "failed"; } const sessionData = normalizeSessionData(data); const current = session.value; const stableData = current.data != null && sessionData != null && isJsonEqual(current.data, sessionData) ? current.data : sessionData; session.set({ data: stableData, error: null, isPending: false, isRefetching: false, refetch }); return outcome; } catch (fetchError) { if (signal.aborted) return "aborted"; const latest = session.value; session.set({ data: latest.data, error: fetchError, isPending: false, isRefetching: false, refetch }); return "failed"; } }; const getFreshUntil = () => { const expiresAt = session.value.data?.session?.expiresAt; const sessionExpiresAt = expiresAt instanceof Date ? expiresAt.getTime() : Number.POSITIVE_INFINITY; return Math.min(Date.now() + SESSION_MOUNT_DEDUPE_INTERVAL, sessionExpiresAt); }; const fetchSession = (queryParams) => { freshUntil = 0; flight?.cancel(); const controller = new AbortController(); const request = { cancel: () => controller.abort(), promise: Promise.resolve().then(() => { if (controller.signal.aborted) return "aborted"; return executeSessionFetch(controller.signal, queryParams); }), revision: sessionRevision }; flight = request; const settleFlight = (outcome) => { if (flight !== request) return; flight = void 0; if (outcome === "fresh" && request.revision === sessionRevision) freshUntil = getFreshUntil(); }; request.promise.then(settleFlight, () => settleFlight("failed")); return request.promise.then(() => void 0); }; const fetchSessionOnMount = () => { if (flight?.revision === sessionRevision) return flight.promise.then(() => void 0); if (Date.now() < freshUntil) return Promise.resolve(); return fetchSession(); }; let broadcastSessionUpdate = () => {}; onMount(session, () => { let timeoutId; if (!isServer()) timeoutId = setTimeout(() => { fetchSessionOnMount(); }, 0); const refreshManager = createSessionRefreshManager({ fetchSession, shouldPollSession: () => session.value.data != null, sessionSignal: $signal, options }); refreshManager.init(); broadcastSessionUpdate = refreshManager.broadcastSessionUpdate; return () => { if (timeoutId) clearTimeout(timeoutId); refreshManager.cleanup(); }; }); return { session, $sessionSignal: $signal, broadcastSessionUpdate: (trigger) => broadcastSessionUpdate(trigger) }; } //#endregion export { getSessionAtom };