fix(pwa): dodge Pages CDN gzip-of-range on the basemap archive (#118)#119
Merged
Conversation
GitHub Pages/Fastly gzip-compresses application/octet-stream and answers Range requests with 206 slices OF THE COMPRESSED stream — un-inflatable in the browser, so first-load/no-SW visitors got a blank vector basemap. - Rename data/basemap.pmtiles -> basemap.pmtiles.png (git mv, identical blob): image/png is the only content-type proven gzip-exempt AND Range-clean on this origin. Loud NOT-a-PNG comments guard the masquerade. - sw.ts matcher now owns BOTH .pmtiles.png and legacy .pmtiles via lib/basemap.ts's isBasemapArchivePath (route order/body unchanged — still first-registered before precacheAndRoute). - New services/basemapSource.ts: uncontrolled-page preflight (Range bytes=0-15, require true 206 + 'PM' magic) with a full-body fetch into a Blob-backed pmtiles Source via protocol.add() on failure; SW-controlled pages skip everything. - MapView mount effect is async: the transport check resolves BEFORE map construction (protocol.add must not race the first tile request); cancelled flag closes the unmount-during-fetch window; fallback-fetch failure surfaces through the existing noteMapError/banner path. - New e2e basemap-fallback.spec.ts (SW-blocked context) simulates the CDN corruption exactly and pins both the forced-fallback and the ranged fast path; offline.spec keeps proving the precache Range path. Closes #118 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DocGerd
commented
Jul 23, 2026
DocGerd
commented
Jul 23, 2026
DocGerd
commented
Jul 23, 2026
DocGerd
commented
Jul 23, 2026
DocGerd
commented
Jul 23, 2026
DocGerd
commented
Jul 23, 2026
DocGerd
commented
Jul 23, 2026
- F1: one try/catch spans the WHOLE async mount — post-await construction throws (WebGL init, control wiring) now route into the existing console.error + noteMapError + one-shot banner path instead of a silent floating rejection. - F2: basemap-fallback e2e proves the Blob-backed map paints REAL tiles — settled-canvas byte compare (datalayers idiom) against a deterministic tiles-less baseline (honest preflight, all other archive reads aborted). - F3: exact end-of-spec totals pinned after the paint proof — corruptedRangeRequests === 1 and fullBodyRequests === 1 (a Protocol key drift would surface as EXTRA ranged FetchSource reads). - F4: new MapView.mount.test.tsx covers the cancelled-flag window (unmount during hanging preflight; preflight resolving post-unmount), the F1 error routing (RED before the fix), and pins StrictMode double-mount (two preflights, single map construction). - F5: the catch logs BEFORE the cancelled check — unmount-mid-fetch no longer swallows the trace; banner/#27 recording stay gated on live mounts. - F6: preflight fetch carries cache:'no-store' (Chrome can synthesize an honest 206 from an HTTP-cached full body, masking a broken CDN as range-ok) — pinned literally in the unit tests. - F7: sw.ts + lib/basemap.ts comments corrected — the legacy .pmtiles shape stays OWNED by the route but degrades to a network fetch on precache miss (404 post-rename), self-healing on the update reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 23, 2026
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.
Closes #118
Problem
GitHub Pages/Fastly gzip-compresses
application/octet-streamand answersRangerequests with 206 slices of the compressed stream. The browser cannot inflate a mid-stream gzip fragment, so every ranged pmtiles read on a first-load/no-SW page failed (net::ERR_CONTENT_DECODING_FAILED) and the vector basemap stayed blank. SW-controlled pages were never affected (ranges are sliced from the precache).Fix (design-workflow decision A4)
app/public/data/basemap.pmtiles→basemap.pmtiles.pngviagit mv(identical blob, no repo growth).image/pngis the only content-type proven gzip-exempt and Range-clean on this origin; Pages derives MIME from the final extension. Loud "this is NOT a PNG" comments inpipeline/extract_basemap.sh,src/lib/basemap.ts, andpipeline/README.mdprevent a future cleanup from reverting it.src/sw.ts's first-registered Range→206 route now matches viaisBasemapArchivePath()(.pmtiles.pngor legacy.pmtiles— an installed SW updating across the rename keeps owning both shapes). Route ORDER and body unchanged — still registered beforeprecacheAndRoute.src/services/basemapSource.ts): a preflight (Range: bytes=0-15, require true 206 +PMmagic) runs once per mount on uncontrolled pages; on failure it warns ([#118], console-only, silent by design — no i18n), fetches the archive once full-body (a COMPLETE gzip stream decodes fine), and registers a Blob-backed pmtilesSourceviaprotocol.add(), keyed on the exact href the style'spmtiles://reference uses. SW-controlled pages skip everything.new MaplibreMap(...)(protocol.addmust not race the first tile request); acancelledflag closes the unmount-during-fetch window; a thrown fallback fetch surfaces through the existingnoteMapError()+ one-shot banner path. All existing wiring/comments preserved.app/e2e/basemap-fallback.spec.ts(SW-blocked context = honest no-SW cohort): Case A simulates the CDN corruption exactly (page.route fulfills ranged requests with a 206 whose body starts1f 8b 08 00) and proves one full-body fetch + painted canvas +[#118]warn + zero decoding errors + no error banner; Case B proves the passing preflight keeps the ranged fast path and the 27 MB full fetch NEVER runs. This spec is what keeps the fallback from rotting while the CDN behaves.Evidence
npm --prefix app run lint— clean.npm --prefix app run typecheck— clean (tsc -b, covers the new e2e spec via tsconfig.e2e.json).npm --prefix app run test— 72 files / 723 tests passed (full run, 249 s; includes the 21 new basemap/basemapSource tests).npm --prefix app run build— precache 25 entries (33 664.40 KiB);dist/data/basemap.pmtiles.png= 27 201 789 bytes; builtsw.jscontains the.pmtiles.png/.pmtilesmatcher and the renamed precache entry.data/basemap.pmtiles.pngstill returns 206 from precache.vite previewat/sail_command/, HTTP-Range-capable):curl -H 'Range: bytes=0-15'on the renamed archive →206,Content-Type: image/png,Content-Range: bytes 0-15/27201789, body startsPMTiles.[#118]console warn present, 0ERR_CONTENT_DECODING_FAILED, map-error banner absent.Notes for review / release
__SC_UAT__gate nor the badge module area / App.tsx.registerType: 'prompt'— download happens in background at install, activation stays user-gated)./uat/probe must pass or explicitly exercise the fallback before release:curl -s -D- -o /tmp/head16 -H 'Accept-Encoding: gzip, deflate, br' -H 'Range: bytes=0-15' https://docgerd.github.io/sail_command/uat/data/basemap.pmtiles.png— REQUIRE HTTP 206, NOcontent-encoding,content-rangetotal exactly 27201789, first 7 body bytes50 4d 54 69 6c 65 73. Run twice (cold + warm CDN cache). If it FAILS, the cache-busted browser pass must instead show the fallback working (one full-body GET, map paints,[#118]warn, noERR_CONTENT_DECODING_FAILED) — users are unbroken either way.docs/superpowers/specs/still referencesbasemap.pmtiles; spec edits are main-session-gated and excluded from this PR..claude/settings.json's artifact guard matches*.pmtilesbut not the renamed*.pmtiles.png— consider extending the pattern so the binary stays hand-edit-protected.PMso the real App tree takes therange-okpath in jsdom.🤖 Generated with Claude Code