Merge pull request 'fix(pwa): network-first navigation so a new deploy loads online immediately' (#200) from feature/sw-fresh-online into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 14s
CI / ui (push) Successful in 1m24s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m43s
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 10s
CI / integration (push) Successful in 14s
CI / ui (push) Successful in 1m24s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m43s
This commit was merged in pull request #200.
This commit is contained in:
@@ -71,6 +71,11 @@ func handlerFor(content fs.FS, stripPrefix, indexName string) http.Handler {
|
|||||||
// app (notably a relaunched Telegram Mini App) is a cache hit, not a re-download.
|
// app (notably a relaunched Telegram Mini App) is a cache hit, not a re-download.
|
||||||
if strings.HasPrefix(name, "assets/") {
|
if strings.HasPrefix(name, "assets/") {
|
||||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
} else if name == "sw.js" {
|
||||||
|
// The service worker must be revalidated on every load so a new deploy's worker (and its
|
||||||
|
// fresh precache manifest) is picked up promptly; a cached sw.js would strand clients on
|
||||||
|
// the old build. The worker's network-first navigation then serves the fresh shell online.
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
}
|
}
|
||||||
files.ServeHTTP(w, r)
|
files.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ func TestHandlerServesServiceWorkerAsJavaScript(t *testing.T) {
|
|||||||
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") {
|
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") {
|
||||||
t.Errorf("Content-Type = %q, want text/javascript", ct)
|
t.Errorf("Content-Type = %q, want text/javascript", ct)
|
||||||
}
|
}
|
||||||
|
// Revalidated on every load so a new deploy's worker (and its fresh precache) is picked up.
|
||||||
|
if cc := resp.Header.Get("Cache-Control"); cc != "no-cache" {
|
||||||
|
t.Errorf("sw.js Cache-Control = %q, want no-cache", cc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHandlerFallsBackToShellForUnknownManifestPath guards the trap: a manifest/sw path NOT emitted
|
// TestHandlerFallsBackToShellForUnknownManifestPath guards the trap: a manifest/sw path NOT emitted
|
||||||
|
|||||||
+22
-7
@@ -8,7 +8,7 @@
|
|||||||
// served stale. Registration stays manual and web-only (pwa.svelte.ts): skipped in the mock build
|
// served stale. Registration stays manual and web-only (pwa.svelte.ts): skipped in the mock build
|
||||||
// and inside a Telegram/VK Mini App.
|
// and inside a Telegram/VK Mini App.
|
||||||
import { clientsClaim } from 'workbox-core';
|
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';
|
import { NavigationRoute, registerRoute } from 'workbox-routing';
|
||||||
|
|
||||||
// The injectManifest build replaces self.__WB_MANIFEST with the precache list; the DOM lib knows
|
// 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.
|
// needs. The landing page and the old-engine polyfill bundle are excluded via the build globs.
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
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.
|
// admin console so those are never resolved to the app shell.
|
||||||
registerRoute(
|
const NAV_TIMEOUT_MS = 3000;
|
||||||
new NavigationRoute(createHandlerBoundToURL('index.html'), {
|
async function freshShell({ request }: { request: Request }): Promise<Response> {
|
||||||
denylist: [/^\/scrabble\.edge\.v1\.Gateway/, /^\/_gm\//],
|
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\//] }));
|
||||||
|
|||||||
Reference in New Issue
Block a user