Files
OneCAD-Draft/web/sw.js
T
Afonsop 5f9b345ee6 feat: restore-all session recovery, ?file= deep links, offline PWA
Session recovery: more than one pending snapshot adds a 'Restore all'
button to the Recover Drawing modal — every snapshot goes through the
exact same guarded path as a single restore (deleted only after its
own open succeeds). The open-tab list and active tab persist to
localStorage on web (open/save/close) and the remembered tab is
re-selected once a restore batch settles; the modal notes how many
drawings the session had. Nothing is auto-opened.

Deep links: ?file=<url|name> opens a drawing at boot. http(s) URLs
must end in .dwg/.dxf and are fetched with the same streaming,
64 MB-capped, 8 s-deadline reader the remote images use, then go
through the guarded open queue; cross-origin opens announce the
origin first. Bare names resolve from the browser's recent storage,
with a hint when absent. Everything else (file:, javascript:, drive
letters, non-drawing tails) is refused with a console note.

Offline PWA: the app was installable (manifest) but downloaded its
55 MB bundle on every visit. A cache-first service worker now serves
the whole bundle from Cache Storage after one online visit —
navigations network-first so updates land, hashed assets
stale-while-revalidate, non-GET/cross-origin passthrough, old caches
cleaned on activate, and the waiting-worker update flow documented.
The build wrapper stamps the cache version from the bundle hash and
hard-fails if sw.js is missing or unstamped.

Also fixes a locale group mismatch on the DATALINK pick notice
(catalog said a display group that does not exist in the ftl
sources).
2026-09-18 16:27:12 +01:00

137 lines
5.4 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);
try {
const fresh = await fetch(request);
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"},
});
})(),
);
});