CreateExplanationFor authorsApplies from 4.2.5
Sub-path, cache and service worker, incomplete deploys, going back
Cette page en françaisThis page on the GitHub wiki
A game that plays well on npm run dev can still break once online: served under /<repo>/ instead of /, kept old
by a service worker, half uploaded, or rolled back over newer saves. This page explains how the build handles each, and
how to tell which one you are looking at.
Sub-path: BASE_PATH
A site at the root of its domain (https://play.example.org/) needs nothing. A site under a path
(https://<user>.github.io/<repo>/) must be built with that path:
# in the created project or the engine repository
BASE_PATH=/my-repo/ npm run buildBASE_PATH (default /) becomes Vite's base (tools/vite/plugins.ts). It is
written into:
- the page's scripts and styles (
/<repo>/assets/index-<hash>.js); index.html's links tomanifest.webmanifestandog.png;- the web app manifest:
start_urland the two icons (<BASE_PATH>icons/icon-192.png,icon-512.png); - the service worker, registered at
<BASE_PATH>sw.js, whose scope is that path.
Images, sounds and videos are addressed relatively (assets/img/<id>.webp?v=…), resolved against the page's address,
so they follow the page wherever it is served, as long as the address ends with /.
Write BASE_PATH with a slash at both ends. The manifest is built by joining strings: BASE_PATH=/my-repo gives
icons at /my-repoicons/….
Cache and service worker
The service worker is generated by vite-plugin-pwa (Workbox) from vite.config.ts.
What it keeps, and how.
| What | When it is cached | Strategy |
|---|---|---|
The app shell: **/*.{js,css,html,ttf,webmanifest}, icons/*.png | At install (precache) | From the cache; a new build brings a new list |
Images, assets/img/ | First use or warm-up | Cache first, up to 2,000 files, 90 days |
Sounds, assets/audio/ | First use or warm-up | Cache first, range requests, up to 600 files, 90 days |
Videos, assets/video/ | First use or warm-up | Cache first, range requests, up to 10 files |
Full fonts, fonts/ | The first time a character needs them | Cache first, up to 10 files |
data/ (the sealed ending's file) | Each request | Network first, the cache after 4 s without an answer |
Left out of the precache: assets/img, assets/audio, assets/video, data, assets/tools (the Studio and the
dev tools), studio-demo, fonts, and assets/reality for a game that declares no reality.
Warm-up. From the title screen and after each room change, the engine fetches in the background the current room,
the rooms directly reachable and their audio, in batches sized by GameDef.assetBudgets. Then, once per page, the rest
of the game (GameDef.offline, default full): every image, effect, voice, music and video, batch by batch while the
browser is idle, paused while the page is hidden. Nothing is fetched on a "save data" or 2G connection; music and video
wait for better than 3G. offline: 'nearby' keeps only the room-scoped warm-up.
Versions. Each image or sound address carries ?v=<hash>: one hash of every file under public/assets, so any
change to an asset gives every asset a new address and the old cache entries are no longer used. The sealed ending's
file is always asked from the network first.
What the player is told. The warm-up ends with a status (App.offlineStatus, offlineReady):
| Status | Meaning |
|---|---|
complete | Every file of the plan is in the cache |
partial | With its reason: network (and the files that failed), save-data, slow, quota (the browser reports less than 64 MB free before it starts) |
skipped | Nothing was tried; worker: the service worker did not take control of the page in time, so fetching would have cached nothing |
off | offline: 'nearby' |
The pause menu shows it on its "Offline" row (ui.offlineStatus; no row with offline: 'nearby'): "312/400…" while
it runs, then "complete", or "312/400 ⚠ retry" (the texts ui.offlineComplete and ui.offlineRetry; the demo game
says "whole game cached" and "tap to retry"). A tap retries a partial or skipped warm-up; a file already in the Cache API is never fetched twice, so the warm-up resumes across reloads with
no state of its own. With offline: 'nearby', a room never visited nor warmed may still need the network.
Updates. The worker claims the page at its first activation (clientsClaim), so the first visit's warm-up goes
through it. A new version never activates on its own (skipWaiting: false): when a new sw.js is found, the game
shows "A new version is ready." with "Save and update", which saves the game, waits for the save to be written, then
activates the new worker and reloads. Old precaches are removed then (cleanupOutdatedCaches).
npm run e2e:pwa (engine repository) requires complete, then checks every file of the plan against the cache
offline and renders a room never visited. It proves this on Chromium; WebKit cannot navigate offline under automation
and reports "skipped" (exit 3), which only the CI's --allow-skip accepts.
Applies from 4.5.0 to …, since 4.5.0
Navigation fallback and /docs/
Offline, the worker answers a navigation inside its scope with the cached index.html (navigateFallback). From
4.5.0 it does not for an address containing /docs/ (navigateFallbackDenylist: [/\/docs\//]): the project's Pages
serves the documentation under /<repo>/docs/, beside the game, and a reader who once played must get that page, not
the game.
For your game, this means: do not serve it under a path that contains /docs/ (for example
https://example.org/docs/my-game/). It plays online, but offline the fallback no longer covers it: a navigation
the precache does not hold under its exact address (one with a query string, for instance) fails.
Incomplete deploy
A deploy is incomplete when the host serves the new index.html (or sw.js) while files it names are missing:
an upload stopped half-way, files copied in the wrong order, a CDN that has the new page and the old scripts.
- A player who already has the game keeps it: their page comes from the worker's precache. The new worker installs only when every file of its precache downloads; until then the old version keeps playing, and the browser tries again at a later visit.
- A new visitor gets the new page, which names scripts that are not there: a blank page, and 404s on
<BASE_PATH>assets/index-<hash>.jsin the browser's network panel. - A page cached by HTTP.
npm start(the engine repository's sirv launcher) sends every file withmax-age=3600: a visitor without the worker may keep an olderindex.htmlfor up to an hour, naming scripts the new build removed.
Avoid it: deploy atomically (GitHub Pages and a Clever Cloud deploy publish a whole build at once), or upload
index.html and sw.js last; serve index.html and sw.js with no-cache
(Other static hosts).
Going back to an earlier version
Deploying the build of an earlier commit puts it back: for the players, it is just another new version.
- Players with the newer version see "A new version is ready." and move back when they click "Save and update". Until then they keep the version they have.
- Their saves stay in the browser (IndexedDB, per site address): going back deletes none.
- A save made by the newer version loads in the older one only if the older one knows how to read it. If the newer
version raised the game's
saveVersion, the older one has no migration for it and refuses that save (save version N cannot be migrated to M): Continue fails for those players until the newer version is back. Saves only migrate forward (State and saves). - A save made in a remix world loads only into that same world.
So: going back is safe when the newer version did not change saveVersion. When it did, prefer fixing forward.
Symptoms, causes and fixes
| Symptom | Likely cause | Diagnosis | Fix |
|---|---|---|---|
Works on npm run dev, blank page under /<repo>/ | Built without BASE_PATH | Network panel: 404 on /assets/index-….js (no /<repo>/) | Build with BASE_PATH=/<repo>/ |
Icons or install missing, /<repo>icons/… 404 | BASE_PATH without its trailing slash | manifest.webmanifest's icons | BASE_PATH=/<repo>/ |
Images missing under /<repo> (no final slash) | The page's address does not end with / | Image URLs resolved to /assets/img/… | Link to /<repo>/; Pages redirects |
| The old version stays after a deploy | The new worker waits for the player | Developer tools: a worker "waiting to activate" | Click "Save and update"; close every tab of the game |
| No update banner at all | sw.js cached by the host or a CDN; or the new worker's precache fails | Network panel on sw.js; the console during install | no-cache on sw.js; complete the upload |
| Blank page for new visitors only | Incomplete deploy | 404 on hashed scripts | Upload the whole dist/ again |
| Offline, a room asks for the network | partial warm-up, or offline: 'nearby' | The pause menu's status | Tap to retry online; or offline: 'full' |
| Continue fails after a rollback | The save is from a newer saveVersion | The console's error | Redeploy the newer version |
Offline, an address of the game under a /docs/… path fails to load | The denylist, by design | The address contains /docs/ | Serve the game under another path |
Check it is fixed
- The browser's developer tools (Application → Service workers) show one active
sw.jsunder your base path, and nothing waiting. - The pause menu's "Offline" row says "complete"; with the network off, a reload plays and a room never visited opens.
- In the engine repository:
npm run e2e:pwaagainst your build, andnode scripts/e2e.mjs https://<your-site>/against the live site.
Neighbouring cases
- A player whose save will not load for other reasons (a world of a remix, a corrupted save): Player troubleshooting.
- Installing the game as an app, and Safari's limits offline: Install as an app.
- How the engine's asset graph and music director use this cache: How the engine works.
Alternatives set aside
- Activating a new version at once (
skipWaiting). It would swap the code under a game in progress. The game asks, saves, then switches. - Precaching every image and sound. The first visit would download the whole game before anything plays. The shell is precached; the rest is warmed in the background, and the player is told when the whole game is there (decision D5, "the whole game offline after the first visit").
- A hash in every asset's file name. The game's ids name the files (
assets/img/<id>.webp); a single?v=overpublic/assetskeeps them stable and still busts the cache when any asset changes, at the cost of fetching every asset again after any asset change.
Trade-offs
- An update reaches a player only when they accept it, or close every tab of the game.
- Any asset change makes every asset's address new: the next full warm-up downloads the whole game again.
/docs/is reserved within the game's scope, for the documentation beside it.
Decisions and ADRs
- D5 (
docs/dev/DECISIONS.md): the whole game offline after the first visit,GameDef.offline. - D50 and ADR 0030: the documentation published under
/docs/beside the game on Pages. - Sources:
vite.config.ts,src/engine/dom/update.ts,src/engine/dom/offline.ts,src/engine/boot.ts,src/engine/core/save.ts,scripts/e2e-pwa.mjs.