fix(sw): complete cache-clearing + add controllerchange auto-reload
All cache-clearing buttons and the IDAA clear-caches page previously cleared IDB/localStorage but left service worker registrations and Cache Storage intact. On the next reload the SW re-served the old JS bundle, leaving users stuck on stale code despite appearing to reload. This caused recurring stale-state reports from IDAA and other clients for 4+ months. Two gaps closed: 1. Every clear path (root page buttons, sys bar, help tech, idaa/clear-caches) now unregisters SW registrations and clears Cache Storage before touching IDB and localStorage. Order: SW → Cache Storage → IDB → localStorage. 2. Added controllerchange listener in +layout.svelte effect 4. When the new SW activates and calls clients.claim(), this listener reloads the page so open tabs run the new JS bundle instead of keeping old code in memory indefinitely. Without this, skipWaiting + clients.claim work correctly on the SW side but the page side never picks up the update. Also added thorough code comments and updated REFERENCE__Common_Agent_Mistakes (#15) and BOOTSTRAP doc (#8) to document the full root cause so this cannot be silently re-broken by a future agent or refactor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -274,6 +274,27 @@ function clear_sess() {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
// CRITICAL — all three storage layers must be cleared together or the fix is incomplete.
|
||||
//
|
||||
// The service worker (SW) maintains its own Cache Storage that is entirely separate from
|
||||
// IndexedDB, localStorage, and sessionStorage. The SW uses Cache Storage to serve the
|
||||
// app's JS/CSS bundles and HTML shell directly from disk — bypassing the network entirely
|
||||
// for those assets. This means:
|
||||
//
|
||||
// Clearing IDB + localStorage ONLY:
|
||||
// → SW is still registered and still serving old JS bundles from its cache.
|
||||
// → Next reload: SW intercepts, returns OLD cached JS → user is still stuck.
|
||||
//
|
||||
// Clearing SW registrations ONLY:
|
||||
// → Cache Storage still contains old entries. A newly registered SW may serve them
|
||||
// before it rebuilds the cache from the network, causing a transient stale state.
|
||||
//
|
||||
// Clearing BOTH SW registrations AND Cache Storage, THEN IDB/localStorage:
|
||||
// → Next reload: no SW intercepts → fresh HTML from server → fresh JS bundle
|
||||
// filenames → fresh JS loaded → user is on current code. ✅
|
||||
//
|
||||
// This function is called by the version-mismatch detection path (flag_hard_reload).
|
||||
// All manual cache-clearing buttons in the app must follow the same order.
|
||||
async function clear_stale_service_worker_state() {
|
||||
if (!browser || sw_heal_in_flight) return;
|
||||
sw_heal_in_flight = true;
|
||||
@@ -307,6 +328,27 @@ async function clear_stale_service_worker_state() {
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
// SW update → page reload bridge. DO NOT REMOVE.
|
||||
//
|
||||
// How SW updates propagate (all three steps are required):
|
||||
// 1. service-worker.js calls skipWaiting() on install → new SW activates immediately
|
||||
// instead of waiting for all existing tabs to close.
|
||||
// 2. service-worker.js calls clients.claim() on activate → new SW takes control of
|
||||
// all currently open tabs without requiring a navigation.
|
||||
// 3. THIS LISTENER: when the new SW takes control, `controllerchange` fires on every
|
||||
// open tab. We reload so the tab runs the new JS bundle rather than keeping the
|
||||
// old JS that was already loaded in memory.
|
||||
//
|
||||
// Without step 3, users can be stuck on old code indefinitely after a deployment —
|
||||
// even though the SW correctly updated under them. The tab shows no error; it just
|
||||
// silently runs whatever JS it had at the time the SW changed. This caused IDAA and
|
||||
// other users to run stale code for weeks at a time across multiple deployments.
|
||||
// Added 2026-06-22 to close this gap.
|
||||
const on_controller_change = () => window.location.reload();
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('controllerchange', on_controller_change);
|
||||
}
|
||||
|
||||
// Initialise PWA install prompt listener (captures beforeinstallprompt early).
|
||||
pwa_install.init();
|
||||
|
||||
@@ -390,7 +432,12 @@ $effect(() => {
|
||||
if (!$ae_loc.is_native) $ae_loc.is_native = true;
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('message', handler);
|
||||
return () => {
|
||||
window.removeEventListener('message', handler);
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.removeEventListener('controllerchange', on_controller_change);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 5. SESSION EXPIRED EFFECT — watches ae_auth_error and raises the banner when the API signals 401/403.
|
||||
|
||||
Reference in New Issue
Block a user