Skip to content

Dynamic cache update #8

Description

@Exeloo

Which app is this feature request for?

client, website

Feature

Right now the game cache is all-or-nothing. Changing one file makes every player download the whole game again.

How it works today:

  • client: the /manifest route (apps/client/src/manifest.ts) returns a list of file paths with no per-file metadata, plus one global version. That version comes from public/version or, if that file is missing, from a fingerprint of every file's path:size:mtime.
  • website: isManifestUpToDate (apps/website/src/manifest.ts) compares this global version with the one saved in localStorage (game-version). If they differ, GameCache._updateCacheProcess (apps/website/src/cache/cache.ts) clears the whole OPFS game directory and downloads every file again.
  • If the versions match, _parseFile only checks that each file can be opened. It never checks whether the file is complete or correct. Two @todo comments in the code already point this out.

The result:

  • Any change (one script, one asset) triggers a full re-download. For games with large assets this makes updates slow and uses a lot of bandwidth.
  • In --watch mode, every save triggers a full re-download, which slows down the dev loop.
  • A download that was interrupted or corrupted can stay in the cache without anyone noticing, because file contents are never checked.

Ideal solution or implementation

Add a content hash for each file and compare hashes on every load.

client

  1. Add hash and size to each manifest entry:
    type IManifestFile = { path: string; hash: string; size: number };
  2. Compute the hash from the file contents with a stable algorithm (e.g. SHA-256 via Bun.CryptoHasher), so it's the same across machines and server restarts.
  3. /manifest currently rescans the directory on every request, so keep an in-memory hash cache keyed by path + size + mtime. Unchanged files then aren't hashed again on each request.
  4. The global version can be derived from the sorted list of file hashes. Keep it for compatibility or as a quick "nothing changed" check.

website

  1. Save the manifest of what's cached locally (path → hash), for example as a JSON file in OPFS or in localStorage next to game-version.
  2. On load, compare the remote manifest with the local one:
    • new or changed hash: download the file
    • same hash: reuse the file already in OPFS
    • gone from the remote manifest: delete it from OPFS
  3. Replace the unconditional directory.clear() in _updateCacheProcess with this diff.
  4. Write the local manifest entry only after the file has been fully written. Then an interrupted update just resumes on the next load instead of leaving the cache inconsistent.
  5. Optional: check the downloaded bytes against the expected hash (crypto.subtle.digest) before saving them. This covers the existing @todo about file completeness.
  6. Show "X / Y files to update" in the loading UI instead of always showing the full count.

force = true should still wipe everything and download it all again.

Alternative solutions or implementations

  • Per-file size + mtime instead of a content hash: cheaper on the client, but less reliable. mtime changes on checkout or copy even when the content is identical, and a file can change without its size changing. It's fine for --watch but not for production.
  • HTTP caching (ETag / If-None-Match on /game/*): this lets the browser skip unchanged downloads, but it still needs one request per file. It also doesn't solve cleanup of the OPFS cache or integrity checks.
  • Keep the global version but diff by path only: easy, but it can't detect files whose content changed.

Other context

  • Relevant files: apps/client/src/manifest.ts, apps/client/src/files.ts, apps/client/src/server.ts (IManifest), apps/website/src/cache/cache.ts, apps/website/src/manifest.ts, apps/website/src/types/manifest.type.ts, apps/website/src/version/version.ts.
  • Changing the manifest format affects both packages, so client and website need to be released together, or the website needs to fall back to a full download when hash is missing.
  • apps/server has a similar file-listing logic. It's out of scope here but could reuse the same hashing helper later.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions