Skip to content

fix(website): stop cross-tab OPFS cache race that breaks asset loading - #101

Open
Tchips46 wants to merge 4 commits into
mainfrom
fix/opfs-cache-race-across-tabs
Open

fix(website): stop cross-tab OPFS cache race that breaks asset loading#101
Tchips46 wants to merge 4 commits into
mainfrom
fix/opfs-cache-race-across-tabs

Conversation

@Tchips46

@Tchips46 Tchips46 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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: isManifestUpToDate now actually returns true on the
    matching-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: added Web Locks (navigator.locks) around the destructive
    rebuild 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).

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
@Tchips46
Tchips46 requested a review from Exeloo as a code owner August 27, 2026 20:45
@github-actions github-actions Bot added the app:website Related to website app label Aug 27, 2026
Same behavior, less commentary in the diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Em3JASkEqn8hgf9Wqx1Sn1
@Tchips46 Tchips46 self-assigned this Aug 27, 2026
MartinFillon
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
@github-actions github-actions Bot added the app:client Related to client app label Aug 27, 2026
@Exeloo Exeloo mentioned this pull request Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:client Related to client app app:website Related to website app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants