Cache battlefield artwork across visits instead of re-fetching 41 MB - #159
Merged
Conversation
Every boot fetched the full HD atlas set — 36 sheets plus their JSON, ~41 MB through four bounded lanes — and the hosting serves those files with `max-age=0, must-revalidate`, so even a warm refresh paid ~85 conditional round-trips, and a cold or evicted HTTP cache paid the whole transfer again. The loading screen promised "usually quickest after the first visit" with nothing behind the promise. The HD build now stamps assetHashes into manifest.json — 16 hex chars of sha256 per emitted file (tools/hd-art/manifestHash.ts) — and the committed manifest is stamped for the shipped files; a test recomputes every stamp so a stale one cannot be committed. On web origins the renderer keeps HD atlas responses in a Cache Storage cache keyed by <file>?v=<hash>. The manifest itself is always revalidated (cache: 'no-cache'), so a warm boot costs one conditional manifest request and zero HD transfers; an art drop re-fetches only the files whose hash changed, and entries the manifest no longer names are pruned after a proven-complete load. Downloaded bytes are verified against their stamped hash before being stored, so a half-updated host can never seed the cache with bytes that disagree with the manifest — the no-mixed-sets completeness contract holds by construction. When revalidation fails but a manifest copy is stored, the match boots from the cached set instead of the recovery screen. localhost origins (the dev server, and the Capacitor shells reading from the app bundle) and environments whose Cache Storage is missing or refuses to open keep exactly the previous network path, as do the baseline pixel atlases, which have no manifest to pin their versions to and revalidate cheaply in parallel. Closes #149 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QhdcsdfhBzJW4VBKWKoRsu Signed-off-by: Achilleas Tsoumitas <knorcedger@gmail.com>
Review of #149 caught fetchVersioned contradicting its own contract: when WebCrypto is unavailable (an insecure LAN playtest origin whose browser still exposes Cache Storage), contentHashOf returns null and the null was accepted as "verified", pinning unverifiable bytes under the new hash key. A half-updated host could then seed the cache with stale art that every later boot serves until the next art drop rotates the key. Bytes now go into the cache only when a computed digest matches the manifest stamp. Unverifiable bytes are still returned — the render proceeds exactly as it would without a store — but never stored, so such origins simply keep today's fetch-every-boot behavior. No warning is logged for the null-digest case: a missing WebCrypto is an environment property, not a host misdeploy. Refs #149 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QhdcsdfhBzJW4VBKWKoRsu Signed-off-by: Achilleas Tsoumitas <knorcedger@gmail.com>
…aching-tyri11 Signed-off-by: Achilleas Tsoumitas <knorcedger@gmail.com>
Three review findings on the #149 cache, all in its failure paths. A transient 503 or 429 on the manifest was read as "this deployment ships no HD art": it skipped the cached-set boot that an equivalent network failure would have used, and erased the stored manifest — the offline anchor — so later offline boots failed too. Only a definitive 404/410 now takes that branch; every other failed status raises and falls into the same cached-manifest fallback as a connection failure. That definitive branch also deleted only the manifest entry, while prune() runs solely after the completeness gate passes — unreachable once the manifest is gone — so ~41 MB of versioned atlas entries would have sat stranded on the device until quota eviction. The path now clears the whole store, which only ever holds HD files and their manifest. Cache Storage awaits also ignored the abort signal. The loaders' deadlines are cooperative, so one wedged cache promise — a real Chromium storage failure mode that rejects nothing and never settles — would have hung the loading screen forever, past every timeout that boundedAssetLoad promises. Every cache await now loses a race against the caller's signal, and through the existing attempt() guard a wedged cache degrades to the plain network path instead. Also corrects an overclaim in ASSET_CONTRACT.md: hash verification protects the cache from poisoning; a half-updated host can still put mismatched bytes into that one session's render, exactly as it could before the store existed. Refs #149 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QhdcsdfhBzJW4VBKWKoRsu Signed-off-by: Achilleas Tsoumitas <knorcedger@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Aug 23, 2026
…aching-tyri11 Signed-off-by: Achilleas Tsoumitas <knorcedger@gmail.com>
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.
What changed?
Every page refresh sat through the full "Loading battlefield artwork…" stage again: the deployment serves the ~41 MB HD atlas set (36 sheets + JSON, through 4 bounded lanes) with
cache-control: public, max-age=0, must-revalidate, so a warm visit paid ~85 conditional round-trips and a cold or evicted HTTP cache re-downloaded everything. Nothing implemented the loading screen's "usually quickest after the first visit" promise.Now:
tools/hd-art/build.tsstampsassetHashesintomanifest.json(16 hex chars of sha256 per emitted file,tools/hd-art/manifestHash.ts); the committed manifest is stamped for the shipped files, and a test recomputes every stamp so a stale one cannot be committed.packages/game/src/artworkStore.ts) keyed by<file>?v=<hash>. The manifest is always revalidated (cache: 'no-cache'), so a warm boot costs one conditional manifest request and zero HD transfers; an art drop re-fetches only files whose hash changed; unreferenced entries are pruned after a proven-complete load.Related issue
Closes #149
Why this approach?
Cache-Controlalone lives in the Vercel dashboard (outside the repo) and either keeps per-file revalidation or risks staleness on un-hashed names.packages/game); the sim is untouched.assetHashes; a manifest without hashes simply keeps the plain fetch path.Verification
npm run typecheck— cleannpm run test— 127 files / 1424 tests passed (11 skipped, pre-existing)npm run build,npm run bundle:check,npm run bundle:self-test— greenNew unit coverage: store hit/miss/verify/prune/clear, wedged-cache degradation, no-WebCrypto behavior, manifest revalidation (write-through, offline fallback, 404-clears-set, 503-keeps-set),
assetHashesparsing, and a committed-manifest hash-truthfulness test over all 72 shipped filesConfirmed the live deployment serves
?v=<hash>URLs correctly (200, full body, correct content type)Not device-tested in this environment; behavior is unchanged on localhost/native by design
npm run typechecknpm testnpm run buildI added or updated tests where behavior changed
I included screenshots or a recording for visible changes — no visual change; loading gets faster on repeat visits
Provenance and AI assistance
Implemented and reviewed with Claude Code under maintainer direction (coordination in #149). No art assets were created or modified; the only asset change is stamping sha256 content hashes of the existing committed files into
manifest.json.Contributor checklist
CONTRIBUTING.mdand followed the deterministic simulation rulesgit commit -s)🤖 Generated with Claude Code
https://claude.ai/code/session_01QhdcsdfhBzJW4VBKWKoRsu
Generated by Claude Code