Files
OneCAD-Draft/web/sw.js
T
Afonsop 369e2c8b5a fix: close the review findings on the restore-all/deep-links/PWA wave
Two reviewers, no blockers, three majors — all fixed.

- The remembered-tab focus after a Restore-all batch fired at the
  message boundary, but the LAST restore's tab inserts one async round
  later (its OPFS recent-cache write precedes the insertion) and the
  insertion's own activation overwrote the selection. The focus flag
  now also consumes at the insertion point, so the batch focus holds
  in the common all-success case.
- The service worker cached ANY successful in-scope navigation under
  the shell key — a direct navigation to a font or JSON would be
  served as the app shell for every offline navigation until the next
  online visit. Only the scope root caches as the shell now.
- Local macOS builds broke: sha256sum does not exist there (shasum
  does) and BSD sed requires an explicit -i suffix. The wrapper picks
  the available hasher and uses a portable in-place edit.

Also: '/app/' gets its own web app manifest (the previous commit's
'installable' claim was true only of the marketing site, whose
start_url sits OUTSIDE the service worker's scope — an installed app
opened offline showed an error page). The README now states the
honest offline timing (assets captured on the second visit — the
first visit's preloads outrun the installing worker) and the
long-lived-window update behavior.
2026-09-18 17:08:27 +01:00

141 lines
5.7 KiB
JavaScript

// Open CAD Studio web app service worker: offline app-shell caching.
//
// Trunk copies this file to dist/app/sw.js (copy-file link in web-app.html),
// so it is served at /app/sw.js and its scope is /app/ — the whole app. The
// page registers it with a relative URL; see the inline script in
// web-app.html.
//
// Caching strategy
// - Navigations (any path under /app/, deep links included): network-first.
// A successful response is cached under the canonical shell URL (the
// scope root) and returned, so updates land on the next visit; offline
// the cached shell is served instead, which also serves as the fallback
// for deep-linked paths.
// - Other same-origin GET requests (wasm, js, workers, fonts, JSON):
// cache-first with a background revalidate. Trunk content-hashes the
// heavy assets (OpenCADStudio-<hash>[_bg].{js,wasm}), so a cached copy
// can never be stale and revalidation is effectively free; non-hashed
// files (fonts/*.ttf, *.json, ocs-*.js) are silently refreshed in the
// background. Everything the app loads is captured during the first
// online visit, after which the ~55 MB bundle is served from the cache
// instead of re-downloaded on every visit.
// - Non-GET and cross-origin requests: passthrough (never intercepted).
//
// Versioning
// CACHE_VERSION below is a placeholder ("v1") so this file works verbatim
// under `trunk serve`. scripts/build-web.sh stamps it with a hash of the
// finished bundle after every build, so each deploy ships a fresh cache
// name and the activate step below deletes the previous deploy's cache.
//
// Update flow
// A changed sw.js installs a new worker, but this code deliberately never
// calls skipWaiting() and never reloads the page: the browser keeps the
// new worker in the "waiting" state until every tab running the old
// version has closed, so an in-progress editing session is never disrupted
// mid-session. The new worker (and its new cache) takes over on the first
// visit after that. Nothing is shown to the user — the browser chrome
// already handles the "update available" UX.
const CACHE_VERSION = "v1"; // stamped by scripts/build-web.sh (bundle hash)
const CACHE_NAME = `ocs-app-${CACHE_VERSION}`;
const SHELL_URL = new URL("./", self.registration.scope);
self.addEventListener("install", (event) => {
// Precache the app shell (index.html) so offline navigation works from the
// first visit after registration. If the shell cannot be fetched the
// install fails and the browser retries on a later navigation — a broken
// shell must never be cached.
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll([SHELL_URL.href])),
);
});
self.addEventListener("activate", (event) => {
// Delete every cache from previous versions, then start controlling pages
// that loaded before this worker was ready, so the current visit's requests
// are captured without needing a reload.
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name)),
);
await self.clients.claim();
})(),
);
});
// Cache a copy of a response unless it is not a complete success. Quota or
// write errors are swallowed: caching must never break the app itself.
const put = async (cache, key, response) => {
if (!response || !response.ok) return;
try {
await cache.put(key, response.clone());
} catch {}
};
self.addEventListener("fetch", (event) => {
const request = event.request;
// Only same-origin GETs are ours to serve.
if (request.method !== "GET") return;
if (new URL(request.url).origin !== self.location.origin) return;
// Range requests (media seeking) cannot be served from a full-body cache.
if (request.headers.has("range")) return;
if (request.mode === "navigate") {
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Only the scope root is the app shell: caching any other in-scope
// navigation here would store e.g. a font or JSON body under the
// shell key and serve it for every offline navigation.
const isShell = new URL(request.url).pathname === SHELL_URL.pathname;
try {
const fresh = await fetch(request);
if (isShell) await put(cache, SHELL_URL, fresh);
return fresh;
} catch {
const cached = await cache.match(SHELL_URL);
if (cached) return cached;
return new Response("Open CAD Studio is offline.", {
status: 503,
headers: {"Content-Type": "text/plain; charset=utf-8"},
});
}
})(),
);
return;
}
// Cache-first with background revalidate. On a hit the cached copy is
// returned immediately and a fresh one is fetched (and cached) in the
// background; on a miss the network response is cached before being
// returned, making the next visit offline-capable.
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const network = fetch(request)
.then(async (fresh) => {
await put(cache, request, fresh);
return fresh;
})
.catch(() => undefined);
if (cached) {
// Keep the worker alive until the background revalidate finishes.
event.waitUntil(network);
return cached;
}
const fresh = await network;
if (fresh) return fresh;
return new Response("Open CAD Studio is offline.", {
status: 503,
headers: {"Content-Type": "text/plain; charset=utf-8"},
});
})(),
);
});