Files
Jyotisha/frontend/src/lib/app-navigation.ts
T

144 lines
4.4 KiB
TypeScript

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<void> {
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<string, unknown>)) {
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);
},
};
}