fix(website): stop cross-tab OPFS cache race that breaks asset loading - #101
Open
Tchips46 wants to merge 4 commits into
Open
fix(website): stop cross-tab OPFS cache race that breaks asset loading#101Tchips46 wants to merge 4 commits into
Tchips46 wants to merge 4 commits into
Conversation
isManifestUpToDate() unconditionally returned false (the up-to-date branch fell through to the same `return false` as the stale branch), and the only caller of GameCache.updateCache() hardcoded force: true. Together these guaranteed every single page load fully cleared and rewrote the OPFS game file cache, even when nothing had changed. OPFS is shared per-origin across every open tab. Two tabs loading around the same time both ran the full clear-and-rewrite unconditionally, racing on the same shared files: one tab's write can invalidate a blob: URL another tab already handed out for the same file. In Chromium this surfaces as a fetch or <img> load failing with net::ERR_UPLOAD_FILE_CHANGED, and that URL can never succeed again - the affected tab ends up with assets (e.g. its game's sprites) permanently failing to load. - isManifestUpToDate: return true on the matching-version branch instead of falling through to false. - index.ts: stop forcing an unconditional rebuild so the (now-working) up-to-date check actually gets used. - GameCache: serialize the destructive rebuild across tabs with the Web Locks API as defense-in-depth, re-checking cache freshness once the lock is held in case a concurrent tab already finished rebuilding for the same manifest while we were waiting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Em3JASkEqn8hgf9Wqx1Sn1
Same behavior, less commentary in the diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Em3JASkEqn8hgf9Wqx1Sn1
MartinFillon
previously approved these changes
Aug 27, 2026
… file getVersion() read public/version and fell back to a hardcoded "0.0.0" when that file didn't exist - which is the common case for local dev/build, since nothing writes it. Combined with the isManifestUpToDate() fix, the loader now correctly trusts a matching version and skips re-downloading - but since the version never actually changed, it would keep serving whatever got cached in OPFS on the very first load, forever, even after the game was rebuilt with different code. Fall back to a fingerprint (Bun.hash) of each served file's size + mtime instead of a constant. Verified against a live server: two /manifest calls with no file changes in between return the same version; editing a file and calling again returns a different one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Em3JASkEqn8hgf9Wqx1Sn1
Exeloo
approved these changes
Aug 28, 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.
Problem
Two tabs on the same origin loading the game around the same time could end up
with one tab's game assets permanently failing to load
Reproducable on Brave
Fix
manifest.ts:isManifestUpToDatenow actually returnstrueon thematching-version branch.
index.ts: stopped forcing an unconditional rebuild, so the (now working)up-to-date check is actually used - a tab whose cache already matches the
current manifest version no longer touches OPFS at all.
cache.ts: addedWeb Locks(navigator.locks) around the destructiverebuild path as defense-in-depth, for the case where multiple tabs do
need to rebuild at the same time (e.g. right after a version bump, or on
first ever load). Only one tab performs the clear-and-rewrite at a time;
a tab that was waiting on the lock re-checks freshness once it acquires it
and reuses the result instead of redundantly rebuilding again. Falls back
to running unlocked on browsers without the Web Locks API (unchanged
behavior from before this PR on those browsers).