better-auth
Version:
The most comprehensive authentication framework for TypeScript.
168 lines (167 loc) • 4.98 kB
JavaScript
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 };