import { isAxiosError } from "axios"; import { REFRESH_TOKEN_COOKIE, clearSessionCookies, getCookie, } from "./cookies"; import { refreshSessionTokens } from "./http"; /** * Proactively refreshes the token pair on a fixed cadence so the server-side * session (a sliding 1-hour window, extended only by /auth/refresh-token) is * kept alive while the app is open. The 401 interceptor in http.ts remains * the reactive fallback; both share the same single-flight refresh call. * * The interval MUST stay well under the server session window (60 min). */ const DEFAULT_INTERVAL_MINUTES = 10; const getIntervalMs = () => { const minutes = Number(import.meta.env.VITE_TOKEN_REFRESH_INTERVAL_MINUTES); return ( (Number.isFinite(minutes) && minutes > 0 ? minutes : DEFAULT_INTERVAL_MINUTES) * 60_000 ); }; let timerId: number | null = null; let lastRefreshAt = 0; const refreshNow = async () => { if (!getCookie(REFRESH_TOKEN_COOKIE)) { // Logged out elsewhere; nothing to keep alive. stopTokenRefreshScheduler(); return; } try { await refreshSessionTokens(); lastRefreshAt = Date.now(); } catch (error) { // Network hiccups are retried on the next tick; only an explicit server // rejection means the session is dead. if (isAxiosError(error) && error.response) { stopTokenRefreshScheduler(); clearSessionCookies(); window.location.replace("/auth"); } } }; /** * Browsers freeze timers in background tabs — a tab waking up past its * refresh deadline refreshes immediately instead of waiting a full interval. */ const onVisibilityChange = () => { if (document.visibilityState !== "visible") return; if (Date.now() - lastRefreshAt >= getIntervalMs()) { void refreshNow(); } }; export const startTokenRefreshScheduler = () => { stopTokenRefreshScheduler(); // Token age is unknown here (fresh login vs. hours-old page reload), so // refresh right away to extend the session window from "now". lastRefreshAt = 0; void refreshNow(); timerId = window.setInterval(() => void refreshNow(), getIntervalMs()); document.addEventListener("visibilitychange", onVisibilityChange); }; export const stopTokenRefreshScheduler = () => { if (timerId !== null) { window.clearInterval(timerId); timerId = null; } document.removeEventListener("visibilitychange", onVisibilityChange); };