222 lines
6.1 KiB
TypeScript
222 lines
6.1 KiB
TypeScript
/**
|
|
* Version shared by the early recovery bootstrap and the regular client-side
|
|
* registration. Keep it in sync with `public/sw.js`; a regression test enforces
|
|
* that boundary.
|
|
*/
|
|
export const SERVICE_WORKER_VERSION = "v5-2026-07-15-web-push";
|
|
|
|
/**
|
|
* Runs before stylesheet discovery. A healthy worker answers one cheap version
|
|
* handshake and performs no update check. A legacy worker gets one update and,
|
|
* if it never takes control, one bounded unregister/register fallback.
|
|
*/
|
|
export const SERVICE_WORKER_RECOVERY_BOOTSTRAP = String.raw`
|
|
(function () {
|
|
var serviceWorker = navigator.serviceWorker;
|
|
var controller = serviceWorker && serviceWorker.controller;
|
|
if (!controller) return;
|
|
|
|
var expectedVersion = "${SERVICE_WORKER_VERSION}";
|
|
var expectedScriptPath = "/sw.js?v=" + encodeURIComponent(expectedVersion);
|
|
var markerKey = "service-worker-recovery:v4";
|
|
var legacyMarkerKey = "service-worker-csp-recovery:v3";
|
|
|
|
if (
|
|
typeof controller.scriptURL === "string" &&
|
|
controller.scriptURL === location.origin + expectedScriptPath
|
|
) {
|
|
try {
|
|
sessionStorage.removeItem(markerKey);
|
|
sessionStorage.removeItem(legacyMarkerKey);
|
|
} catch (error) {}
|
|
return;
|
|
}
|
|
|
|
var requestId = Date.now().toString(36) + ":" + Math.random().toString(36).slice(2);
|
|
var handshakeTimer;
|
|
var lockTimer;
|
|
var replacementTimer;
|
|
var recoveryStarted = false;
|
|
var replacementStarted = false;
|
|
var healthy = false;
|
|
var reloaded = false;
|
|
|
|
function clearTimer(timer) {
|
|
if (timer) clearTimeout(timer);
|
|
}
|
|
|
|
function removeHandshake() {
|
|
clearTimer(handshakeTimer);
|
|
try {
|
|
serviceWorker.removeEventListener("message", onMessage);
|
|
} catch (error) {}
|
|
}
|
|
|
|
function clearMarkers() {
|
|
try {
|
|
sessionStorage.removeItem(markerKey);
|
|
sessionStorage.removeItem(legacyMarkerKey);
|
|
} catch (error) {}
|
|
}
|
|
|
|
function cleanup() {
|
|
clearTimer(lockTimer);
|
|
clearTimer(replacementTimer);
|
|
removeHandshake();
|
|
try {
|
|
serviceWorker.removeEventListener("controllerchange", reloadOnce);
|
|
} catch (error) {}
|
|
clearMarkers();
|
|
}
|
|
|
|
function reloadOnce() {
|
|
if (reloaded || healthy) return;
|
|
reloaded = true;
|
|
cleanup();
|
|
location.reload();
|
|
}
|
|
|
|
function acquireLock() {
|
|
try {
|
|
var now = Date.now();
|
|
var rawMarker = sessionStorage.getItem(markerKey);
|
|
if (rawMarker) {
|
|
var marker = JSON.parse(rawMarker);
|
|
if (marker && typeof marker.expiresAt === "number" && marker.expiresAt > now) {
|
|
return marker.expiresAt - now;
|
|
}
|
|
}
|
|
sessionStorage.setItem(markerKey, JSON.stringify({ expiresAt: now + 3000 }));
|
|
} catch (error) {}
|
|
return 0;
|
|
}
|
|
|
|
function requestSkipWaiting(worker) {
|
|
if (!worker) return;
|
|
try {
|
|
worker.postMessage({ type: "HIVEGRID_SW_SKIP_WAITING" });
|
|
} catch (error) {}
|
|
}
|
|
|
|
function finishHealthy() {
|
|
if (healthy || reloaded) return;
|
|
healthy = true;
|
|
cleanup();
|
|
}
|
|
|
|
function probeBeforeReplacement(registration) {
|
|
if (healthy || reloaded) return;
|
|
clearTimer(handshakeTimer);
|
|
requestId = Date.now().toString(36) + ":" + Math.random().toString(36).slice(2);
|
|
try {
|
|
controller.postMessage({ type: "HIVEGRID_SW_VERSION_REQUEST", requestId: requestId });
|
|
handshakeTimer = setTimeout(function () {
|
|
replaceRegistration(registration);
|
|
}, 1500);
|
|
} catch (error) {
|
|
replaceRegistration(registration);
|
|
}
|
|
}
|
|
|
|
function replaceRegistration(registration) {
|
|
if (replacementStarted || reloaded || healthy) return;
|
|
replacementStarted = true;
|
|
var detached = false;
|
|
|
|
Promise.resolve()
|
|
.then(function () {
|
|
return registration ? registration.unregister() : true;
|
|
})
|
|
.then(
|
|
function (unregistered) {
|
|
detached = Boolean(unregistered);
|
|
},
|
|
function () {}
|
|
)
|
|
.then(function () {
|
|
return serviceWorker.register("/sw.js?v=" + encodeURIComponent(expectedVersion), {
|
|
scope: "/",
|
|
updateViaCache: "none"
|
|
});
|
|
})
|
|
.then(function (nextRegistration) {
|
|
if (healthy || reloaded) return;
|
|
requestSkipWaiting(nextRegistration.waiting);
|
|
replacementTimer = setTimeout(function () {
|
|
if (detached) reloadOnce();
|
|
else cleanup();
|
|
}, 1500);
|
|
})
|
|
.catch(function () {
|
|
if (detached) {
|
|
reloadOnce();
|
|
return;
|
|
}
|
|
cleanup();
|
|
});
|
|
}
|
|
|
|
function updateRegistration(registration) {
|
|
Promise.resolve()
|
|
.then(function () {
|
|
return registration.update();
|
|
})
|
|
.then(function () {
|
|
if (healthy || reloaded) return;
|
|
requestSkipWaiting(registration.waiting);
|
|
probeBeforeReplacement(registration);
|
|
})
|
|
.catch(function () {
|
|
probeBeforeReplacement(registration);
|
|
});
|
|
}
|
|
|
|
function startRecovery() {
|
|
if (recoveryStarted || reloaded || healthy) return;
|
|
clearTimer(handshakeTimer);
|
|
var lockDelay = acquireLock();
|
|
if (lockDelay > 0) {
|
|
lockTimer = setTimeout(startRecovery, lockDelay + 10);
|
|
return;
|
|
}
|
|
|
|
recoveryStarted = true;
|
|
try {
|
|
serviceWorker.addEventListener("controllerchange", reloadOnce);
|
|
serviceWorker
|
|
.getRegistration()
|
|
.then(function (registration) {
|
|
if (registration) updateRegistration(registration);
|
|
else replaceRegistration(null);
|
|
})
|
|
.catch(function () {
|
|
replaceRegistration(null);
|
|
});
|
|
} catch (error) {
|
|
cleanup();
|
|
}
|
|
}
|
|
|
|
function onMessage(event) {
|
|
var data = event && event.data;
|
|
if (!data || data.type !== "HIVEGRID_SW_VERSION_RESPONSE" || data.requestId !== requestId) return;
|
|
if (event.source && event.source !== controller) return;
|
|
if (data.version === expectedVersion) {
|
|
finishHealthy();
|
|
return;
|
|
}
|
|
startRecovery();
|
|
}
|
|
|
|
try {
|
|
serviceWorker.addEventListener("message", onMessage);
|
|
controller.postMessage({ type: "HIVEGRID_SW_VERSION_REQUEST", requestId: requestId });
|
|
handshakeTimer = setTimeout(startRecovery, 250);
|
|
} catch (error) {
|
|
startRecovery();
|
|
}
|
|
})();
|
|
`
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|