export type AppNavigationHost = { assign: (href: string) => void; }; export type AppNavigationClick = { button: number; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean; defaultPrevented: boolean; preventDefault: () => void; }; const UNKNOWN = "unknown"; let clientCommitOverride: string | null = null; let knownServerCommit: string | null = null; let host: AppNavigationHost = { assign: (href) => { window.location.assign(href); }, }; export function clientBuildCommit(): string { if (clientCommitOverride !== null) return clientCommitOverride; const value = process.env.NEXT_PUBLIC_GIT_COMMIT; return typeof value === "string" ? value.trim() : ""; } export function knownServerBuildCommit(): string | null { return knownServerCommit; } export function rememberServerBuildCommit(commit: string | null): void { if (typeof commit !== "string") { knownServerCommit = null; return; } const trimmed = commit.trim(); knownServerCommit = trimmed ? trimmed : null; } export function isUsableBuildCommit(value: string | null | undefined): value is string { return typeof value === "string" && value.length > 0 && value !== UNKNOWN; } export function isStaleClientBuild( client = clientBuildCommit(), server = knownServerCommit, ): boolean { if (!isUsableBuildCommit(client) || !isUsableBuildCommit(server)) return false; return client !== server; } export function readHealthGitCommit(payload: unknown): string | null { if (!payload || typeof payload !== "object" || !("deployment" in payload)) return null; const deployment = (payload as { deployment?: unknown }).deployment; if (!deployment || typeof deployment !== "object" || !("gitCommit" in deployment)) return null; const commit = (deployment as { gitCommit?: unknown }).gitCommit; return typeof commit === "string" && commit.trim() ? commit.trim() : null; } export async function syncBuildCommitFromHealth( fetchImpl: typeof fetch = fetch, ): Promise { try { const response = await fetchImpl("/api/health", { cache: "no-store" }); if (!response.ok) return; const payload: unknown = await response.json().catch(() => null); const commit = readHealthGitCommit(payload); if (commit) rememberServerBuildCommit(commit); } catch { // Health is unavailable: keep the last known commit and do not force a reload. } } export function navigateAppPath( href: string, clientNavigate?: (href: string) => void, ): void { if (isStaleClientBuild()) { host.assign(href); return; } if (clientNavigate) { clientNavigate(href); return; } host.assign(href); } function isModifiedClick(event: AppNavigationClick): boolean { return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; } export function interceptAppNavigation(event: AppNavigationClick, href: string): void { if (event.defaultPrevented || isModifiedClick(event)) return; if (!href.startsWith("/") || href.startsWith("//")) return; if (!isStaleClientBuild()) return; event.preventDefault(); host.assign(href); } export function hrefFromLinkTarget(href: unknown): string { if (typeof href === "string") return href; if (!href || typeof href !== "object") return ""; const record = href as { pathname?: unknown; search?: unknown; hash?: unknown; query?: unknown }; const pathname = typeof record.pathname === "string" ? record.pathname : ""; let search = typeof record.search === "string" ? record.search : ""; if (!search && record.query && typeof record.query === "object") { const params = new URLSearchParams(); for (const [key, value] of Object.entries(record.query as Record)) { if (typeof value === "string") params.set(key, value); } const serialized = params.toString(); search = serialized ? `?${serialized}` : ""; } const hash = typeof record.hash === "string" ? record.hash : ""; return `${pathname}${search}${hash}`; } export function setAppNavigationHostForTests(next: AppNavigationHost | null): void { host = next ?? { assign: (href) => { window.location.assign(href); }, }; } export function setClientBuildCommitForTests(commit: string | null): void { clientCommitOverride = commit; } export function resetAppNavigationForTests(): void { clientCommitOverride = null; knownServerCommit = null; host = { assign: (href) => { window.location.assign(href); }, }; }