Skip to content

feat: SW install robustness — runtime-cached glyphs + one-shot recovery (#27, #28)#41

Merged
DocGerd merged 4 commits into
mainfrom
feat/27-28-sw-install-robustness
Jul 16, 2026
Merged

feat: SW install robustness — runtime-cached glyphs + one-shot recovery (#27, #28)#41
DocGerd merged 4 commits into
mainfrom
feat/27-28-sw-install-robustness

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Fixes the two service-worker robustness issues from the live v0.1.0 verification: the first-install budget blowout (#28) and the pre-SW basemap failure window (#27).

Closes #27
Closes #28

What changed

#28 — install shrinks from 791 entries / ~44 MB to 24 entries / ~33 MB. It turned out 768 of the 791 precache entries were font glyph-range .pbf files (the issue estimated ~500). They move to a runtime CacheFirst route (sailcommand-glyphs-v1), scoped to same-origin /basemap-assets/fonts/**.pbf only — the Open-Meteo origin can never match (unit-pinned). A build-emitted glyph-manifest.json (768 sorted paths) drives a window-side background warm-up: after the SW takes control, at browser idle, missing ranges are fetched in polite batches of 8 (bails offline, non-fatal on failure, resumes next visit) so full offline coverage converges without blocking install. Retired glyph caches (sailcommand-glyphs-* ≠ current) are deleted on activate — no leaked megabytes on future version bumps.

#27 — one-shot recovery. MapLibre errors that occur while the page is not yet SW-controlled (the window where GitHub Pages' gzip-stamped Range responses break tiles) are recorded; on the first controllerchange the page reloads exactly once (sessionStorage guard written before the reload — loop-proof; never armed when already controlled at load).

Invariants preserved (the hard rules)

  • .pmtiles Range→206 route stays literally the first registration — verified in the built dist/sw.js in both review rounds.
  • The SW still never caches the Open-Meteo origin; wind stays per-plan in IndexedDB.
  • Nothing blocks install/activate/claim: the warm-up is entirely window-side; the cache cleanup is a bounded activate-time delete that doesn't gate clients.claim().
  • Existing users: the old SW keeps controlling until the update is accepted (registerType: 'prompt'); stale font precache entries are pruned on activation; worst case is a one-time re-fetch of rendered glyphs.

Honest offline testing, strengthened

The server-kill offline spec now waits on deterministic signals before killing (warm-up terminal state + runtime cache ≥ 768 manifest entries — a dead warm-up cannot pass), probes a glyph range offline (must be a 200 from cache against a dead server), and asserts zero console errors across the whole offline phase. The fixture-less built-output guards additionally assert: no font entries in the precache manifest, entry count < 400, and glyph-manifest ↔ dist cross-checks in both directions.

Verification

  • typecheck / lint clean; full unit suite 378 → 384 passed (16 new warm-up/recovery tests + 6 glyph-predicate tests) incl. realmask.repro.
  • Full e2e 4/4 (offline reload with the new gates: 4.0 s); fixture restored.
  • Real browser (built app, fresh profile): warm-up done, 768 glyph cache entries, no Open-Meteo cache, 0 console errors, labels rendering.
  • Per-PR reviewer: round 1 Approve (all five hard rules verified in source and built output, existing-user update path walked through); round 2 Approve (activate cleanup verified in the minified worker, claim ordering unaffected, nothing new).

🤖 Generated with Claude Code

Patrick Kuhn and others added 2 commits July 16, 2026 16:57
…re-SW map loads

#28 — service-worker install robustness:
- Exclude basemap-assets/fonts/** (768 glyph-range .pbf files) from the
  precache manifest via globIgnores: 791 entries / ~44.0 MB -> 24 entries
  / ~33.3 MB, so the install fits the browser's install-event budget even
  on slow connections.
- Serve glyphs from a dedicated runtime CacheFirst route in sw.ts
  (sailcommand-glyphs-v1), scoped same-origin + fonts path + .pbf so it
  can never match the Open-Meteo origin or the .pmtiles Range route,
  which stays the first registration.
- Emit dist/glyph-manifest.json at build time (vite plugin) and backfill
  un-cached ranges from the window side (glyphWarmup.ts) once the SW
  controls the page and the app is idle — never from install/activate
  waitUntil. Failures are non-fatal and retried next visit; the warm-up
  never runs offline.
- Offline e2e stays honest (server-kill pattern): waits on the warm-up's
  deterministic completion signal + runtime cache convergence before the
  server dies, probes a glyph range offline (200 from cache), and asserts
  zero console errors across the whole offline phase. Built-output guards
  now parse the injected precache manifest (no fonts entries, < 400
  entries) and cross-check glyph-manifest.json against dist.

#27 — one-shot recovery from gzip-stamped Range responses:
- swRecovery.ts records MapLibre errors that happen while the page is not
  yet SW-controlled and, on the FIRST controllerchange, reloads the page
  exactly once (sessionStorage guard set before reloading; never arms when
  already controlled at load; never loops; MapView's console.error and
  error banner behavior unchanged).

Refs #27, #28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… size comment

Review round 1 on the #27/#28 change set, two Minors:

- M1: glyphs.ts documented that a GLYPH_CACHE_NAME bump requires deleting
  the old cache, but nothing implemented it — cleanupOutdatedCaches() only
  manages precache caches. sw.ts now deletes retired sailcommand-glyphs-*
  caches in an activate waitUntil (bounded: one caches.keys() + targeted
  deletes; clientsClaim()'s own listener still claims immediately). The
  deletion predicate isRetiredGlyphCache() lives in glyphs.ts as a pure
  function with unit tests (sw.ts has no unit-test harness), alongside
  tests pinning isGlyphPath()'s route scoping.
- M2: main.tsx storage-persist comment updated from the stale "~44 MB"
  to ~33 MB precache + up to ~14 MB glyph runtime cache.

Refs #27, #28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread app/src/services/glyphWarmup.ts
Comment thread app/src/services/glyphWarmup.ts
Comment thread app/src/services/glyphWarmup.ts
Comment thread app/src/services/glyphWarmup.ts Outdated
Comment thread app/src/services/swRecovery.ts Outdated
Comment thread app/src/services/swRecovery.ts
Comment thread app/src/lib/glyphs.ts Outdated
Comment thread app/vite.config.ts Outdated
Comment thread app/vite.config.ts
@DocGerd

DocGerd commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Out-of-diff self-review findings (docs the PR forgot), accepted for the fix wave:

  1. README.md "First load / offline" (~lines 52–54) — MEDIUM: still says the first visit precaches "roughly 44 MB … map fonts/sprites". After this PR the precache is ~33 MB / 24 entries and the ~11 MB of fonts are runtime-cached + background-warmed. Rewording per the lens suggestion (keeping the total-download figure with the precache/runtime split made explicit).
  2. CLAUDE.md PWA section — enhancement: add one bullet documenting the glyph runtime cache (sailcommand-glyphs-*, CacheFirst, background warm-up) and the never-extend-install invariant, so the rule-laden SW guidance stays current.

Patrick Kuhn and others added 2 commits July 16, 2026 17:34
…ps, fix stale numbers

Lens round on PR #41 (code / silent-failure / comment-accuracy):

- glyphWarmup: console.warn on every unusable-manifest path (non-ok,
  malformed, thrown) and in scheduleGlyphWarmup's catch-all — warn, not
  error, so the offline e2e's error collector stays meaningful; the warm-up
  is a degraded-but-working state, never a silent one. Tests pin the warns.
- vite.config glyphManifest(): an empty/missing fonts directory now fails
  the build (this.error) — deploy is build-gated only, so a fontless
  offline experience must not ship silently.
- Dev-mode skip misattribution fixed in glyphs.ts, vite.config.ts, main.tsx
  and the warm-up outcome doc/test title: in dev the warm-up parks forever
  in whenControlled() (no SW ever registers); the manifest-404 path only
  runs under a stale controlling SW.
- Numbers re-measured: glyph cache is ~11 MB (was written as ~14 MB) in
  glyphs.ts, sw.ts, main.tsx, vite.config.ts; precache "~30 MB" -> "~33 MB";
  BATCH_SIZE size claim corrected (avg ~14 KB, CJK-heavy tail > 50 KB);
  "install/activate must stay instant" -> "must not grow" (install awaits
  the ~33 MB precache).
- swRecovery: dangling "issue's never list" pointer fixed; accepted-cost
  comment added (reload discards planner form state / in-flight compute;
  IndexedDB plans safe; fires once per session on a visibly broken page).
- glyphs.ts version-bump checklist now names the offline.spec.ts cache-name
  literal explicitly.
- README "First load / offline": ~33 MB precache + ~11 MB background font
  runtime cache (#28), ~45 MB total eventual download.
- CLAUDE.md PWA section: glyph runtime-cache bullet incl. the
  never-extend-install invariant and bump-procedure pointer.

Refs #27, #28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DocGerd
DocGerd merged commit b51c338 into main Jul 16, 2026
2 checks passed
@DocGerd
DocGerd deleted the feat/27-28-sw-install-robustness branch July 17, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant