Skip to content

fix(pwa): harden service worker cache lifecycle - #187

Merged
shauryagangrade merged 2 commits into
StudentSuite:mainfrom
AyobamiH:fix/service-worker-cache-lifecycle
Aug 23, 2026
Merged

fix(pwa): harden service worker cache lifecycle#187
shauryagangrade merged 2 commits into
StudentSuite:mainfrom
AyobamiH:fix/service-worker-cache-lifecycle

Conversation

@AyobamiH

@AyobamiH AyobamiH commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #169.

What changed

  • Generates public/sw.js during prebuild from a service-worker template.
  • Bakes a unique build ID into the service-worker source so each deployment produces byte-different worker code and can trigger the browser's service-worker update lifecycle.
  • Keeps registration on the stable /sw.js URL.
  • Uses explicit studymap-app-* and studymap-tiles-* cache namespaces so cleanup cannot match generic caches belonging to another application.
  • Migrates the existing app-studymap-v1 and tiles-studymap-v1 cache names explicitly during activation.
  • Removes only stale StudyMap-owned caches while preserving the active deployment caches.
  • Adds /map to the app-shell precache.
  • Deletes all tile entries above TILE_LIMIT.
  • Serializes tile-cache mutations so concurrent fetches cannot race the 300-entry limit.
  • Surfaces service-worker registration failures instead of swallowing them.
  • Tests the generated service-worker runtime rather than duplicating its logic.
  • Proves different build IDs produce different service-worker bytes.
  • Proves generic caches such as app-another-product and tiles-another-product survive StudyMap cleanup.

Verification

Final commit:

301616c690686e23bb15f3c1dd16acbad3d8c544

Validated against the current upstream base:

  • 14 test files passed
  • 117 tests passed
  • 4 service-worker cache-lifecycle tests passed
  • npm run build passed
  • npm run lint passed
  • npx tsc --noEmit passed
  • place-data validation passed
  • upstream CI passed
  • upstream Lint & Typecheck passed

The production build also verifies that prebuild generates public/sw.js with the build ID embedded before next build runs.

Validation PR:
AyobamiH#7

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

@AyobamiH is attempting to deploy a commit to the studentsuite Team on Vercel.

A member of the Team first needs to authorize it.

@AyobamiH
AyobamiH force-pushed the fix/service-worker-cache-lifecycle branch from 72a5d66 to 4bf0378 Compare August 22, 2026 19:24

@shauryagangrade shauryagangrade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: SW cache lifecycle (#169)

First, the genuinely good parts — several real bugs fixed:

  • trimCache now drains all excess instead of one entry per call. The old if (keys.length > max) delete(keys[0]) could never catch up under burst traffic → unbounded tile cache growth. Real fix.
  • Prefix-scoped cleanup with explicit legacy names (app-studymap-v1 / tiles-studymap-v1) stops the activate handler from deleting unrelated caches on a shared origin. Correct citizenship, and the rename bridge is covered.
  • Serialized tile mutations close the limit-check race; /map precache is valid (route exists at src/app/map/page.tsx); registration errors surfaced instead of swallowed; testing the real sw.js in a vm context rather than reimplementing it is exactly right.

Blocking: the versioned script URL can't trigger reinstalls by itself

The stated goal — "a deploy activates a fresh cache namespace even when public/sw.js is unchanged" — doesn't hold per the service worker update algorithm. An update installs only when the fetched script's bytes differ from the current worker's cached bytes:

If newestWorker is not null and its cached bytes equal the fetched response body → HasNoChangedResource, abort.

The comparison is on response bytes only — query string differences are irrelevant (fragments have explicit handling in the spec; queries do not). So after this ships:

  1. Deploy N registers /sw.js?v=shaN — installs fine.
  2. Deploy N+1 (sw.js untouched) registers /sw.js?v=shaN+1. Browser fetches it, bytes are identical to the current worker's cached script → HasNoChangedResource → old worker stays.
  3. The surviving worker's self.location.href still carries v=shaN, so VERSION keeps resolving to the old namespace forever. Stale app-shell caches are never rotated — the exact failure mode #169 describes.

What ?v= does legitimately solve is HTTP-cache staleness of the script fetch itself — but Vercel already serves public/ assets revalidated, and it does nothing for byte identity.

Suggested fix

Make the bytes change every deploy instead of the URL — inject the build ID into the file at build time:

// scripts/build-sw.mjs — run in "prebuild"
const template = readFileSync("public/sw.template.js", "utf8");
writeFileSync("public/.sw-generated.js",
  template.replace("__BUILD_ID__", process.env.VERCEL_GIT_COMMIT_SHA ?? ...));

…then either register the generated path (/sw.generated.js, stable URL) or copy over public/sw.js before next build emits it. Either way VERSION becomes a plain baked-in const — no location.search parsing needed — and every deploy produces byte-different script → guaranteed install → activate rotates namespaces.

The existing tests survive unchanged apart from sourcing the generated file.

Happy to re-review quickly once the injection lands — everything else here is merge-ready.

@shauryagangrade

Copy link
Copy Markdown
Member

@AyobamiH Please work on the changes requested above, we'll review them when you commit, and merge if everything's in order. :-)

Signed-off-by: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com>
@AyobamiH
AyobamiH force-pushed the fix/service-worker-cache-lifecycle branch from 4bf0378 to 301616c Compare August 22, 2026 20:38
@AyobamiH

Copy link
Copy Markdown
Contributor Author

Addressed the service-worker byte-identity blocker in 301616c. /sw.js is now generated during prebuild with the build ID baked into the worker source, while registration stays on the stable /sw.js URL. Added a regression proving different build IDs produce different worker bytes. Final validation is green: 117/117 tests, build, lint, typecheck and data validation.

@shauryagangrade shauryagangrade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of d7f315c: the $\-pattern nit is resolved via a replacer function (template.replace(TOKEN, () => JSON.stringify(buildId))). Verified locally that build IDs containing $&, $`, $', $1 now render verbatim and produce valid JS (node --check); 4/4 service-worker lifecycle tests pass; eslint clean; CI green on head.

Note for transparency: this follow-up commit was pushed directly to the PR branch by a maintainer as part of the review cycle.

Merging.

@shauryagangrade
shauryagangrade merged commit 43f9188 into StudentSuite:main Aug 23, 2026
4 of 5 checks passed
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.

bug: service worker maintenance gaps — VERSION never bumped, trimCache under-trims, PRECACHE omits /map

2 participants