Files
plainleaf/client/service_worker/util.ts
T
2026-08-12 16:52:16 +08:00

124 lines
3.8 KiB
TypeScript

export async function unregisterServiceWorkers() {
if (navigator.serviceWorker) {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
await registration.unregister();
console.log("Service worker unregistered");
}
} else {
console.error("No service worker found to unregister");
}
}
/** Returns a versioned worker URL so every release triggers an update check. */
export function versionedServiceWorkerURL(
version: string,
baseURI = document.baseURI,
): URL {
const url = new URL("service_worker.js", baseURI);
url.searchParams.set("v", version);
return url;
}
function waitForControllerChange(timeoutMs: number): Promise<boolean> {
return new Promise((resolve) => {
let settled = false;
const finish = (didChange: boolean) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
navigator.serviceWorker.removeEventListener(
"controllerchange",
onControllerChange,
);
resolve(didChange);
};
const onControllerChange = () => finish(true);
const timeout = setTimeout(() => finish(false), timeoutMs);
navigator.serviceWorker.addEventListener(
"controllerchange",
onControllerChange,
);
});
}
function activateWorker(worker: ServiceWorker) {
if (worker.state === "installed") {
worker.postMessage({ type: "skip-waiting" });
return;
}
worker.addEventListener("statechange", () => {
if (worker.state === "installed") {
worker.postMessage({ type: "skip-waiting" });
}
});
}
/**
* Forces a worker update before reloading. If activation cannot complete, the
* registration is removed so the reload comes directly from the server. The
* IndexedDB-backed note cache is deliberately preserved.
*/
export async function reloadWithUpdatedServiceWorker(timeoutMs = 5000) {
if (!navigator.serviceWorker) {
location.reload();
return;
}
const registration = await navigator.serviceWorker.getRegistration(
document.baseURI,
);
if (!registration) {
location.reload();
return;
}
try {
await registration.update();
const replacement = registration.waiting ?? registration.installing;
if (replacement) {
const controllerChanged = waitForControllerChange(timeoutMs);
activateWorker(replacement);
if (!(await controllerChanged)) await registration.unregister();
} else {
// The page can still be running an old JS bundle after the newest worker
// has already activated. A direct reload is the deterministic recovery.
await registration.unregister();
}
} catch (error) {
console.warn("Service worker update failed, reloading directly", error);
await registration.unregister();
}
location.reload();
}
export function flushCachesAndUnregisterServiceWorker() {
console.log("Going to flush the caches and unregister the service worker");
return new Promise<void>((resolve) => {
if (!navigator.serviceWorker?.controller) {
console.log("No service worker active");
return resolve();
}
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data.type === "cacheFlushed") {
console.log("Cache flushed");
// Then unregister all service workers
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (const registration of registrations) {
registration.unregister();
console.log("Service worker unregistered");
resolve();
}
});
}
});
// First flush active cache
navigator.serviceWorker.ready.then((registration) => {
console.log("Sending request to flush cache");
registration.active!.postMessage({ type: "flush-cache" });
});
});
}