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
- Add
hash and size to each manifest entry:
type IManifestFile = { path: string; hash: string; size: number };
- 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.
/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.
- 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
- 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.
- 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
- Replace the unconditional
directory.clear() in _updateCacheProcess with this diff.
- 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.
- Optional: check the downloaded bytes against the expected hash (
crypto.subtle.digest) before saving them. This covers the existing @todo about file completeness.
- 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.
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:
/manifestroute (apps/client/src/manifest.ts) returns a list of file paths with no per-file metadata, plus one globalversion. That version comes frompublic/versionor, if that file is missing, from a fingerprint of every file'spath:size:mtime.isManifestUpToDate(apps/website/src/manifest.ts) compares this global version with the one saved inlocalStorage(game-version). If they differ,GameCache._updateCacheProcess(apps/website/src/cache/cache.ts) clears the whole OPFSgamedirectory and downloads every file again._parseFileonly checks that each file can be opened. It never checks whether the file is complete or correct. Two@todocomments in the code already point this out.The result:
--watchmode, every save triggers a full re-download, which slows down the dev loop.Ideal solution or implementation
Add a content hash for each file and compare hashes on every load.
client
hashandsizeto each manifest entry:Bun.CryptoHasher), so it's the same across machines and server restarts./manifestcurrently rescans the directory on every request, so keep an in-memory hash cache keyed bypath + size + mtime. Unchanged files then aren't hashed again on each request.versioncan be derived from the sorted list of file hashes. Keep it for compatibility or as a quick "nothing changed" check.website
path → hash), for example as a JSON file in OPFS or inlocalStoragenext togame-version.directory.clear()in_updateCacheProcesswith this diff.crypto.subtle.digest) before saving them. This covers the existing@todoabout file completeness.force = trueshould still wipe everything and download it all again.Alternative solutions or implementations
size + mtimeinstead 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--watchbut not for production.ETag/If-None-Matchon/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.Other context
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.hashis missing.apps/serverhas a similar file-listing logic. It's out of scope here but could reuse the same hashing helper later.