mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
Until now `/sw.js` registered only behind the Web-Push opt-in, so any user who never enabled notifications ran with no service worker — the offline shell cache, the precached app shell, and the network-first page cache were all inert. Add a production-gated `ServiceWorkerRegistrar` that installs the worker for everyone, deferred to the `load` event wrapped in `requestIdleCallback` so install/precache stays off the first-paint critical path. Mount it beside `VersionPoller` in the root providers. The registrar only installs. `VersionPoller` remains the sole owner of the update→reload path (poll `/api/version`, then unregister + cache-wipe + reload on a server-ahead deploy); no `updatefound`/`controllerchange` handling lives here so the two cannot double-fire. `updateViaCache: "none"` forces a network byte-check of `sw.js` on every update check so a cached worker can never mask a new deploy.
82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
|
|
/**
|
|
* Pure predicate for whether app-wide SW registration should proceed.
|
|
* Extracted so the production + capability gating is unit-testable without
|
|
* a DOM/effect harness. The registrar's `useEffect` calls this before it
|
|
* touches `navigator.serviceWorker`.
|
|
*/
|
|
export function shouldRegisterServiceWorker(
|
|
nodeEnv: string | undefined,
|
|
hasWindow: boolean,
|
|
hasServiceWorker: boolean,
|
|
): boolean {
|
|
if (nodeEnv !== "production") return false;
|
|
if (!hasWindow) return false;
|
|
if (!hasServiceWorker) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* App-wide service-worker registration.
|
|
*
|
|
* Until this component shipped, `/sw.js` was registered exactly once —
|
|
* inside the Web-Push opt-in (`web-push-card.tsx`). Every user who never
|
|
* enabled notifications ran with no SW at all, so the offline shell cache,
|
|
* the precached app shell, and the network-first page cache were inert. This
|
|
* mounts the registration for everyone, gated on production.
|
|
*
|
|
* Lifecycle owner split: this component only INSTALLS the worker. The
|
|
* update→reload path stays solely with `<VersionPoller>` (polls
|
|
* `/api/version`, then unregister + cache-wipe + reload on a server-ahead
|
|
* deploy). No `updatefound` / `controllerchange` handling lives here — a
|
|
* second reload trigger would double-fire against the poller and risk a loop.
|
|
*/
|
|
export function ServiceWorkerRegistrar(): null {
|
|
useEffect(() => {
|
|
// Production only: dev lacks the generated `/sw-version.js` (written by
|
|
// the `prebuild` step), and a caching SW fights HMR. `process.env.
|
|
// NODE_ENV` is inlined into the client bundle at build time.
|
|
if (
|
|
!shouldRegisterServiceWorker(
|
|
process.env.NODE_ENV,
|
|
typeof window !== "undefined",
|
|
typeof navigator !== "undefined" && "serviceWorker" in navigator,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const register = () => {
|
|
const idle =
|
|
(window as unknown as { requestIdleCallback?: typeof setTimeout })
|
|
.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1));
|
|
idle(() => {
|
|
navigator.serviceWorker
|
|
// `updateViaCache: "none"` forces the browser to byte-check
|
|
// `/sw.js` (and its `importScripts('/sw-version.js')`) against the
|
|
// network on every update check rather than honouring the HTTP
|
|
// cache — without it a cached `sw.js` can mask a new deploy and the
|
|
// worker never sees the new `CACHE_VERSION`.
|
|
.register("/sw.js", { scope: "/", updateViaCache: "none" })
|
|
.catch(() => {
|
|
/* best effort — registration failure must never break the app */
|
|
});
|
|
});
|
|
};
|
|
|
|
// Defer to `load` (+ idle) so install/precache never contends with the
|
|
// first-paint critical path. If the window already fired `load`, run now.
|
|
if (document.readyState === "complete") {
|
|
register();
|
|
return;
|
|
}
|
|
window.addEventListener("load", register, { once: true });
|
|
return () => window.removeEventListener("load", register);
|
|
}, []);
|
|
|
|
return null;
|
|
}
|