The refresh that fixed itself
“I have to hard-refresh three times to see my change.” That was true, and it was our fault — the browser had no way to tell a new build from an old one, so it kept the old one. The fix is a boring, load-bearing piece of plumbing: every file now carries a fingerprint of its own contents in its name. Unchanged files cache for a year; changed ones are simply new files; and when a fresh deploy does land, your tab reloads itself, once. No more knuckle-cracking ⌘-Shift-R.
Why the same URL was a trap
The game's code shipped under fixed names — game.js,
three.js, style.css. A browser that has seen
game.js before is entitled to reuse the copy it already
has, and it does. To force everyone onto a new build we hand-typed a
version number into the offline cache and bumped it on every deploy.
That number was the only thing that shipped an update, and a
human typing a number on every deploy gets it wrong. The service worker
that manages the offline cache carries a 65-line comment that is nothing
but a graveyard of the times we forgot: a decoder that ran off the end
of a buffer because it was a version behind the wire format; a phone
stuck for a week with half the new controls and half the old ones,
because it saw the same version string twice and concluded “nothing
new.”
Underneath that was a quieter bug with the same shape. The offline cache was too greedy: it also pocketed the live data feeds — the daily missions, the leaderboard, your pilot card — and served the first copy it ever saw. The code that fetched them politely asked for a fresh copy every time; the cache sat in front of that request and ignored it. So the leaderboard could be hours stale and no amount of reloading the page would fix it.
Let the file name carry the truth
The new build fingerprints every code and style file: it runs the
contents through a hash and bakes that hash into the name, so
game.js becomes game.BwJYIK_a.js. Change one
line and the hash changes and it's a different file at a different
address — nothing to invalidate, because nobody was ever
holding that address. Change nothing and the name is identical, so the
browser is now told, truthfully, to keep it for a year and never ask
again. The fingerprint is the cache key. The hand-typed version
number is gone, and with it the entire class of “forgot to
bump” incidents — the shell's generation is now computed from
the contents of the shell, so it can't disagree with itself.
The pages themselves are still written by hand with the plain names, so the source stays readable; the server swaps in the fingerprinted names as it hands each page out. And the offline cache was put on a diet: it now keeps only the app itself and never the live data feeds, so the leaderboard and the daily missions always come straight off the wire.
And the reload does itself
Fingerprints make a stale file impossible, but there was still the last mile: a tab you already had open. When a new build lands, it installs quietly in the background, and the page you're looking at keeps running the old one until something tells it to reload — which used to be you, guessing, three times. Now the moment the new version takes over, the tab reloads once, on its own. One automatic refresh instead of a manual game of chance.
There's a treat in here for us, too. During development the offline cache used to hide our own changes — you'd rebuild, reload, and get yesterday's code with a straight face. On a local machine the cache is now switched off entirely, so every reload is honestly the latest build. That one change quietly deletes the most common sentence in our own bug reports.
What the process caught
Two honest confessions. The auto-reload, written naively, reloaded on a visitor's very first load — the new offline cache takes control for the first time on a first visit, which looks exactly like an update if you're not careful, and the page bounced. It only takes a reload now when a build it was already running gets replaced.
The second one the browser tests found: our itch.io export copies the
page's files straight out with no server in the middle to swap the plain
names for fingerprinted ones. The first fingerprinted build would have
shipped an index.html pointing at a game.js
that no longer existed — a blank screen on the embed. The export
now does the same name-swap the server does. We caught it because the
rule here is to boot the real thing in a real browser and watch it load,
every time; the screenshot of the game flying on hashed files is the
proof the plumbing holds.
Same game. Fewer refreshes — ideally, none you have to do yourself.