Skip to content

fix(#939): derive the service worker's cache prefix from one authored string - #947

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/939-sw-prefix-upstream
Aug 23, 2026
Merged

fix(#939): derive the service worker's cache prefix from one authored string#947
TortoiseWolfe merged 1 commit into
mainfrom
fix/939-sw-prefix-upstream

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Closes #939.

The ticket's premise had already changed, so I checked before porting

#922 (PR #946) already fixed the live bug. Verified on a throwaway fixture carrying both halves of the coupling, rebranded with the fixed script:

public/sw.js                  const CACHE_VERSION = 'geolarp-v1.0.0';
                              cacheName.startsWith('geolarp-')
scripts/stamp-sw-version.mjs  const REQUIRED_PREFIX = 'geolarp-';

.mjs is inside the sweep now, so both halves rebrand together and agree. The failure #939 described cannot reproduce on a fork rebranded with the fixed script. I have corrected #939's body and revised its severity rather than leave it claiming a live leak.

Why this is still worth landing

The two strings now agree because both happen to be rewritten in the same sweep. That is incidental, not structural. One rebrand:keep, one hand edit, or one move into a file the sweep does not reach, and they part again — silently, because the damage is only visible in a browser's storage.

Deriving one from the other removes the second copy instead of re-synchronising it. CACHE_VERSION is now the only place the brand appears; CACHE_PREFIX derives from it, and both purge sites and IMAGE_CACHE derive from that.

Two things wrong independently of #922

The guard could not fail.

const cacheVersion = `${REQUIRED_PREFIX}v${version}-…`;
if (!cacheVersion.startsWith(REQUIRED_PREFIX)) {   // tautology

cacheVersion was built from REQUIRED_PREFIX. That is why the divergence shipped with a check sitting right beside it. It now refuses to stamp when it cannot parse a prefix out of the worker — a condition that can actually occur.

Nothing related what the build stamps to what the worker purges. So nothing could have caught it.

The new test never names a brand

That is deliberate: sw-cache-prefix-agrees.test.js asserts the worker derives rather than hard-codes, the stamp script reads rather than declares, and — behaviourally — that stamping a worker carrying an arbitrary brand yields a version with that same brand. The third would have caught the original defect precisely because it does not care what the brand is.

tests/unit/sw-images.test.ts required IMAGE_CACHE to be a literal starting with the brand. That check was the coupling — two hand-synchronised strings. It now asserts IMAGE_CACHE is built from CACHE_PREFIX, and that CACHE_PREFIX strips the -v tail so #438 cannot return through a per-build image cache name.

Mutation-tested on this tree

mutation result
worker hard-codes its purge prefix 1 fail
stamp script re-declares a prefix (the shipped bug) 2 fails
IMAGE_CACHE from the per-build version (#438) 2 fails

All return green on restore.

Behaviour upstream is unchanged, which is the point

A real production build still stamps scripthammer-v0.0.1-<sha>, and all three cache names remain purge-eligible. This is hardening, not a behaviour change.

568/568  pnpm test:scripts
  7/7    tests/unit/sw-images.test.ts
 exit 0  type-check, production build
  clean  gitleaks

Ported from TortoiseWolfe/geolarp#35, where it was built and proven first.

… string

The prefix that names the worker's caches and the prefix its activate handler
purges were two separate strings in two files, kept in agreement by hand. In a fork
they fell out of agreement and shipped: rebrand.sh could not reach `.mjs`, so
public/sw.js was rebranded and scripts/stamp-sw-version.mjs was not. The build then
stamped caches under one brand while the purges matched another. Nothing ever
matched, and every deploy left caches that could never be collected -- unbounded
storage growth in visitors' browsers, observed live on a real fork.

WHAT THIS IS, AND IS NOT. #922 already closed that specific path: discovery is now
`git ls-files`, so both files are swept together and a freshly rebranded fork gets
matching prefixes. Verified on a throwaway fixture carrying both halves. So this is
NOT a live-bug fix upstream, and #939 has been corrected to say so.

It is worth doing anyway, for a reason worth stating precisely: the two strings now
agree BECAUSE both happen to be rewritten in the same sweep. That is incidental, not
structural. One `rebrand:keep`, one hand edit, or one move into a file the sweep does
not reach, and they part again -- silently, because the damage is only visible in a
browser's storage. Deriving one from the other removes the second copy instead of
re-synchronising it.

CACHE_VERSION is now the only place the brand appears. CACHE_PREFIX derives from it,
and both purge sites and IMAGE_CACHE derive from that.

Two things were also wrong independently of #922. The guard could not fail:
`cacheVersion.startsWith(REQUIRED_PREFIX)` where cacheVersion was BUILT from
REQUIRED_PREFIX. That tautology is why the divergence shipped with a check sitting
next to it; it now refuses to stamp when it cannot parse a prefix out of the worker,
a condition that can actually occur. And nothing related what the build stamps to
what the worker purges, so nothing could have caught it.

scripts/__tests__/sw-cache-prefix-agrees.test.js closes that gap and deliberately
never names a brand: the worker must derive rather than hard-code, the stamp script
must read rather than declare, and -- behaviourally -- stamping a worker carrying an
ARBITRARY brand must yield a version with that same brand. The third is the one that
would have caught the original defect, precisely because it does not care what the
brand is.

tests/unit/sw-images.test.ts required IMAGE_CACHE to be a literal starting with the
brand. That check WAS the coupling. It now asserts IMAGE_CACHE is built from
CACHE_PREFIX, and that CACHE_PREFIX strips the -v tail so #438 cannot return through
a per-build image cache name.

Mutation-tested on this tree: hard-coding the purge prefix fails one test,
re-declaring a prefix in the stamp script fails two, and deriving IMAGE_CACHE from
the per-build version fails two. All return green on restore.

Behaviour upstream is unchanged, which is the point: a real build still stamps
`scripthammer-v0.0.1-<sha>` and all three cache names remain purge-eligible.

Ported from TortoiseWolfe/geolarp#35, where it was built and proven first.

Closes #939

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fork service workers never purge their caches: sw.js is rebranded but stamp-sw-version.mjs is not (.mjs is outside the sweep)

2 participants