Run development on the dev server's code, not a build's cache
A production deploy leaves its service worker on the device, and it claims every tab. It serves /.bunext/public/* cache first without knowing which build asked, while every cache-busting mechanism the app has (the precache gate, /build-info.json, the loading page) is production only. So switching to `bun run dev` kept serving the old build, vendor files included, and wrote the dev files back into the production build's cache. Development now clears that worker and every cache it wrote (`clear-development-service-worker.ts`, from ServiceWorkerRegistrar, which did nothing in development before), reloads once into the uncontrolled tab, and /sw.js answers with a stub that unregisters itself so Web Push and the loading page can't register it back.
This commit is contained in:
@@ -7,13 +7,30 @@ import Span from "@moduletrace/twui/layout/Span";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { PwaServiceWorkerPath } from "@/src/data/pwa-data";
|
||||
import clearDevelopmentServiceWorker from "@/src/functions/frontend/clear-development-service-worker";
|
||||
|
||||
const UpdateCheckMs = 60 * 60 * 1000;
|
||||
/**
|
||||
* Development only: waits out a `clients.claim` that raced the clear, so
|
||||
* the clearing reload doesn't come back controlled by the old worker.
|
||||
*/
|
||||
const DevelopmentClearRetryMs = 3000;
|
||||
/**
|
||||
* Development only: guards against reloading in a loop when the worker
|
||||
* can't be cleared (a restricted profile, say).
|
||||
*/
|
||||
const DevelopmentClearAttemptsKey = "cobble-dev-sw-clear";
|
||||
const DevelopmentClearMaxAttempts = 3;
|
||||
|
||||
/**
|
||||
* Registers the app's service worker (production only, so development
|
||||
* HMR is never cached) and offers a reload once a new version is
|
||||
* installed. It never reloads on its own: a bot reply may be running.
|
||||
*
|
||||
* In development it does the opposite: it clears the worker and the
|
||||
* caches a production deploy left on the device, so the dev server's
|
||||
* code is never served from a build's cache (see
|
||||
* `clear-development-service-worker.ts`).
|
||||
*/
|
||||
export default function ServiceWorkerRegistrar() {
|
||||
const [update_ready, setUpdateReady] = useState(false);
|
||||
@@ -78,6 +95,71 @@ export default function ServiceWorkerRegistrar() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV == "production") return;
|
||||
if (typeof navigator == "undefined") return;
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
|
||||
let cancelled = false;
|
||||
let retry: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
/**
|
||||
* A worker claimed this tab while clearing or right after it:
|
||||
* clear again, then reload into the uncontrolled tab. Without
|
||||
* this, the reload would land back on the cached build.
|
||||
*/
|
||||
const onControllerChange = () => {
|
||||
if (cancelled) return;
|
||||
|
||||
retry = setTimeout(() => {
|
||||
if (cancelled) return;
|
||||
|
||||
clearDevelopmentServiceWorker()
|
||||
.then((cleared) => {
|
||||
if (!cancelled && cleared) window.location.reload();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, DevelopmentClearRetryMs);
|
||||
};
|
||||
|
||||
navigator.serviceWorker.addEventListener("controllerchange", onControllerChange);
|
||||
|
||||
clearDevelopmentServiceWorker()
|
||||
.then((cleared) => {
|
||||
if (cancelled) return;
|
||||
if (!cleared) {
|
||||
window.sessionStorage.removeItem(DevelopmentClearAttemptsKey);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This load is still the old worker's code: one reload
|
||||
* takes the dev server's, twice at most.
|
||||
*/
|
||||
const attempts = Number(
|
||||
window.sessionStorage.getItem(DevelopmentClearAttemptsKey) || 0,
|
||||
);
|
||||
|
||||
if (attempts >= DevelopmentClearMaxAttempts) return;
|
||||
|
||||
window.sessionStorage.setItem(
|
||||
DevelopmentClearAttemptsKey,
|
||||
String(attempts + 1),
|
||||
);
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retry) clearTimeout(retry);
|
||||
navigator.serviceWorker.removeEventListener(
|
||||
"controllerchange",
|
||||
onControllerChange,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reloading) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user