Skip to content
quest'n'click

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 build

BASE_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 to manifest.webmanifest and og.png;
  • the web app manifest: start_url and 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.

WhatWhen it is cachedStrategy
The app shell: **/*.{js,css,html,ttf,webmanifest}, icons/*.pngAt install (precache)From the cache; a new build brings a new list
Images, assets/img/First use or warm-upCache first, up to 2,000 files, 90 days
Sounds, assets/audio/First use or warm-upCache first, range requests, up to 600 files, 90 days
Videos, assets/video/First use or warm-upCache first, range requests, up to 10 files
Full fonts, fonts/The first time a character needs themCache first, up to 10 files
data/ (the sealed ending's file)Each requestNetwork 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):

StatusMeaning
completeEvery file of the plan is in the cache
partialWith its reason: network (and the files that failed), save-data, slow, quota (the browser reports less than 64 MB free before it starts)
skippedNothing was tried; worker: the service worker did not take control of the page in time, so fetching would have cached nothing
offoffline: '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

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>.js in the browser's network panel.
  • A page cached by HTTP. npm start (the engine repository's sirv launcher) sends every file with max-age=3600: a visitor without the worker may keep an older index.html for 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

SymptomLikely causeDiagnosisFix
Works on npm run dev, blank page under /<repo>/Built without BASE_PATHNetwork panel: 404 on /assets/index-….js (no /<repo>/)Build with BASE_PATH=/<repo>/
Icons or install missing, /<repo>icons/… 404BASE_PATH without its trailing slashmanifest.webmanifest's iconsBASE_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 deployThe new worker waits for the playerDeveloper tools: a worker "waiting to activate"Click "Save and update"; close every tab of the game
No update banner at allsw.js cached by the host or a CDN; or the new worker's precache failsNetwork panel on sw.js; the console during installno-cache on sw.js; complete the upload
Blank page for new visitors onlyIncomplete deploy404 on hashed scriptsUpload the whole dist/ again
Offline, a room asks for the networkpartial warm-up, or offline: 'nearby'The pause menu's statusTap to retry online; or offline: 'full'
Continue fails after a rollbackThe save is from a newer saveVersionThe console's errorRedeploy the newer version
Offline, an address of the game under a /docs/… path fails to loadThe denylist, by designThe 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.js under 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:pwa against your build, and node scripts/e2e.mjs https://<your-site>/ against the live site.

Neighbouring cases

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= over public/assets keeps 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