forked from 1b-agent/opencadstudio-upstream
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).
137 lines
5.4 KiB
JavaScript
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"},
|
|
});
|
|
})(),
|
|
);
|
|
});
|