Skip to content

Add service worker + version-aware update flow for the PWA#63

Open
radiantnode wants to merge 1 commit into
mainfrom
claude/tensies-pwa-ios-staleness-ng1fsp
Open

Add service worker + version-aware update flow for the PWA#63
radiantnode wants to merge 1 commit into
mainfrom
claude/tensies-pwa-ios-staleness-ng1fsp

Conversation

@radiantnode

Copy link
Copy Markdown
Owner

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 update lifecycle/sw.js carries 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 /static assets are cache-first (safe because their URL changes when their bytes do). activate() purges prior builds' caches.
  • Welcome-frame build check — the WS welcome frame 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'] in static/js/update.js, a one-line change to widen — and otherwise shows a tap-to-refresh banner. The new worker is held in waiting until 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 prebuilt dist/index.html content hash. /sw.js is 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.js route, root scope, Cache-Control: no-cache + Service-Worker-Allowed: /, build id substituted at request time.
  • server/assets.pycurrent_build_id() (dev live hash / prod dist hash / BUILD_ID override) + <meta app-build> injection.
  • server/ws.pywelcome frame carries build.
  • 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:

  • SW registers at scope /, activates, controls the page; no spurious first-install reload.
  • Cache populates on the controlled load (63 assets: shell + 38 JS + 15 CSS + font); old cache deleted on each new build.
  • Landing path: bumped the build → auto-reloaded to the new id, single cache remaining.
  • Mid-game path: in a lobby, bumped the build → banner shown, no reload, new worker held in waiting, old worker still controlling. Tapped banner → reloaded to the new build with the game session preserved (reconnected into the lobby).
  • Server: meta / sw.js / welcome build ids all consistent; /sw.js headers correct. Zero console errors. Prod build_assets.mjs still builds ($build_id preserved, update.css folded into the bundle). tsc strict checkJs clean; 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 + notificationclick handlers, VAPID keys, a push_subscriptions table, and pushsubscriptionchange / 410 Gone pruning, gated on standalone-install + a user-gesture permission prompt (iOS requirements).

🤖 Generated with Claude Code


Generated by Claude Code

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants