fix(pwa): network-first navigation so a new deploy loads online immediately
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s

The C1 service worker served the precached shell for every navigation, even
online, so a new deploy only reached a client after the worker updated its
precache in the background (a one-launch lag): a cold online start showed the
old version until then, and only a reinstall forced it fresh.

- sw.ts: navigations are now network-first — fetch the fresh (no-cache) shell
  from the server on each online launch (it references the new hashed assets,
  fetched fresh), with a 3s timeout falling back to the precached shell when the
  network is unreachable or too slow. Only the tiny HTML is re-fetched; the
  immutable hashed assets stay cache-first and re-download only when their hash
  (the version) changes. Offline cold-launch still works via the fallback.
- webui.go: serve sw.js with Cache-Control: no-cache so the browser reliably
  detects a new deploy's worker (regression-tested).

Online launch is contour-verified (the mock e2e disables the SW); e2e 196.
No new deps — the network-first handler is hand-written (~12 lines).
This commit is contained in:
Ilia Denisov
2026-07-06 16:45:07 +02:00
parent fd225564a3
commit 020742fad3
3 changed files with 31 additions and 7 deletions
+22 -7
View File
@@ -8,7 +8,7 @@
// served stale. Registration stays manual and web-only (pwa.svelte.ts): skipped in the mock build
// and inside a Telegram/VK Mini App.
import { clientsClaim } from 'workbox-core';
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { cleanupOutdatedCaches, matchPrecache, precacheAndRoute } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
// The injectManifest build replaces self.__WB_MANIFEST with the precache list; the DOM lib knows
@@ -28,10 +28,25 @@ cleanupOutdatedCaches();
// needs. The landing page and the old-engine polyfill bundle are excluded via the build globs.
precacheAndRoute(self.__WB_MANIFEST);
// Serve the precached shell for any in-scope top-level navigation. Deny-list the RPC path and the
// Navigations are network-first so a new deploy loads immediately when online: fetch the fresh
// (no-cache) shell from the server — it references the new hashed assets, which are then fetched
// fresh — and only fall back to the precached shell when the network is unreachable or too slow (a
// short timeout). This keeps the app up to date online while still cold-launching offline. Only the
// tiny HTML is re-fetched; the immutable hashed assets stay cache-first (precacheAndRoute above) and
// re-download only when their hash — i.e. the version — changes. Deny-list the RPC path and the
// admin console so those are never resolved to the app shell.
registerRoute(
new NavigationRoute(createHandlerBoundToURL('index.html'), {
denylist: [/^\/scrabble\.edge\.v1\.Gateway/, /^\/_gm\//],
}),
);
const NAV_TIMEOUT_MS = 3000;
async function freshShell({ request }: { request: Request }): Promise<Response> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), NAV_TIMEOUT_MS);
try {
const res = await fetch(request, { signal: ctrl.signal });
if (res.ok) return res;
} catch {
/* offline or too slow — serve the precached shell below */
} finally {
clearTimeout(timer);
}
return (await matchPrecache('index.html')) ?? Response.error();
}
registerRoute(new NavigationRoute(freshShell, { denylist: [/^\/scrabble\.edge\.v1\.Gateway/, /^\/_gm\//] }));