Add service worker + version-aware update flow for the PWA#63
Open
radiantnode wants to merge 1 commit into
Open
Add service worker + version-aware update flow for the PWA#63radiantnode wants to merge 1 commit into
radiantnode wants to merge 1 commit into
Conversation
Ship a root-scoped service worker (phases 1 + 2) so the installed PWA stays fresh — the groundwork for web push later — without the classic "stuck on an old build until you reinstall" trap. Freshness is guaranteed two ways, sharing one response: - The SW's own update lifecycle: /sw.js carries the current build id, so a deploy changes its bytes and the browser installs a new worker. The HTML document is always network-first (newest index -> newest hashed asset URLs); only content-hashed /static assets are cache-first. activate() purges prior builds' caches. - The WS `welcome` frame now reports the server build id; the client compares it against the <meta name="app-build"> the page loaded with — a check that fires even on an iOS warm resume where the SW hasn't polled. On a detected update the client reloads immediately only on a safe screen (AUTO_RELOAD_SCREENS = ['landing'], one edit to change) and otherwise shows a tap-to-refresh banner, so a live round is never yanked out from under the player. The waiting worker is held until the page decides it's safe, so assets don't swap mid-game. Single build id source: dev derives it from the live static-tree hash (matches the ?v= cache-buster); prod reads the prebuilt dist/index.html content hash. No Dockerfile/nginx/build-pipeline changes — /sw.js is app-served in both modes and caching is runtime, so no build-time asset manifest is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dke2mXWgLep4yXsNrT1gry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The installed PWA had a manifest but no service worker, so a warm iOS resume can sit on an old build indefinitely (iOS freezes the standalone page in memory and resumes it without reloading the document). This adds a root-scoped service worker and a version-aware update flow so the app stays fresh — and lays the groundwork for web push later — built so the SW can never become the stale thing (the classic "reinstall to fix it" trap).
What
Two freshness signals, sharing one response:
/sw.jscarries the current build id, so a deploy changes its bytes → the browser installs a new worker. The HTML document is always network-first (newest index → newest hashed asset URLs); only content-hashed/staticassets are cache-first (safe because their URL changes when their bytes do).activate()purges prior builds' caches.welcomeframe now reports the server build id; the client compares it against the<meta name="app-build">the page loaded with. This fires even on an iOS warm resume where the SW hasn't polled, and when no SW is available at all.Reload safely, never mid-game. On a detected update the client reloads immediately only on a safe screen —
AUTO_RELOAD_SCREENS = ['landing']instatic/js/update.js, a one-line change to widen — and otherwise shows a tap-to-refresh banner. The new worker is held inwaitinguntil the page decides it's safe, so assets never swap under a live round.Single build-id source, no prod plumbing churn. Dev derives the id from the live static-tree hash (matches the existing
?v=cache-buster); prod reads the prebuiltdist/index.htmlcontent hash./sw.jsis app-served in both modes (in prod nginx proxies it to the app) and caching is runtime, so there are no Dockerfile / nginx / build-pipeline changes and no build-time asset manifest to maintain.Changes
static/sw.js(new) — the service worker: network-first navigations, cache-first hashed assets, old-cache purge, page-controlled activation.static/js/update.js(new) — registration, update lifecycle, the idle policy, the banner, and the welcome-frame build compare.static/css/update.css(new) — banner styling (hidden by default; CSP-safe, no inline styles).main.py—/sw.jsroute, root scope,Cache-Control: no-cache+Service-Worker-Allowed: /, build id substituted at request time.server/assets.py—current_build_id()(dev live hash / prod dist hash /BUILD_IDoverride) +<meta app-build>injection.server/ws.py—welcomeframe carriesbuild.server/routes.py,static/index.html,static/js/app.js,static/js/net.js,static/js/types.js— wiring.Verification
Exercised in real Chromium at the mobile viewport (390×844) against the docker-compose stack:
/, activates, controls the page; no spurious first-install reload.waiting, old worker still controlling. Tapped banner → reloaded to the new build with the game session preserved (reconnected into the lobby).meta/sw.js/welcomebuild ids all consistent;/sw.jsheaders correct. Zero console errors. Prodbuild_assets.mjsstill builds ($build_idpreserved,update.cssfolded into the bundle).tscstrictcheckJsclean; Python compiles.Follow-up (not in this PR)
This registers the SW across the install base, so web push (Phase 3) can later ship as an ordinary SW update — no reinstall. That work would add
push+notificationclickhandlers, VAPID keys, apush_subscriptionstable, andpushsubscriptionchange/410 Gonepruning, gated on standalone-install + a user-gesture permission prompt (iOS requirements).🤖 Generated with Claude Code
Generated by Claude Code