/**
* Extension update check: the server publishes the current version at
* /api/ext/manifest; when it is newer than the installed extension, the popup
* shows an "update available" banner (the core background/content scripts are
* not hot-updatable in MV3 — updating means installing a new zip).
*/
import browser from "webextension-polyfill";
import { compareVersions } from "@ltt/shared";
import { getSettings, saveSettings, type Settings } from "./settings";
const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12h
interface ExtManifest {
version?: string | null;
assets?: unknown;
}
/** GET /api/ext/manifest with a hard timeout; null when unreachable. */
export async function fetchExtManifest(serverUrl: string, timeoutMs = 5000): Promise<ExtManifest | null> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${serverUrl.replace(/\/+$/, "")}/api/ext/manifest`, {
signal: controller.signal,
});
if (!response.ok) return null;
return (await response.json()) as ExtManifest;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
/**
* Fetches the server manifest and stores the update state. Never throws —
* a check is a background nicety, not a feature the UI waits on.
*/
export async function checkForUpdate(): Promise<{ remoteVersion: string | null; updateAvailable: boolean }> {
try {
const settings = await getSettings();
const manifest = await fetchExtManifest(settings.serverUrl);
const remoteVersion = typeof manifest?.version === "string" ? manifest.version : null;
const updateAvailable = Boolean(
remoteVersion && compareVersions(remoteVersion, browser.runtime.getManifest().version) > 0
);
await saveSettings({ remoteVersion, updateAvailable, lastVersionCheckAt: Date.now() });
return { remoteVersion, updateAvailable };
} catch {
return { remoteVersion: null, updateAvailable: false };
}
}
/** Check on service-worker start, throttled to CHECK_INTERVAL_MS. */
export async function maybeCheckForUpdate(): Promise<void> {
try {
const settings = await getSettings();
if (Date.now() - (settings.lastVersionCheckAt ?? 0) < CHECK_INTERVAL_MS) return;
await checkForUpdate();
} catch {
// storage may be unavailable early in the SW lifecycle — ignore
}
}
/** Re-check when the server (or panel) URL changes in settings. */
export async function checkAfterSettingsChange(patch: Partial<Settings>): Promise<void> {
if ("serverUrl" in patch || "panelUrl" in patch) await checkForUpdate();
}