Skip to content

fix: key the app-shell cache off a content-derived build id - #979

Merged
FSM1 merged 1 commit into
mainfrom
fix/974-rotate-app-shell-cache
Aug 3, 2026
Merged

fix: key the app-shell cache off a content-derived build id#979
FSM1 merged 1 commit into
mainfrom
fix/974-rotate-app-shell-cache

Conversation

@FSM1

@FSM1 FSM1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #974

The defect

APP_SHELL_CACHE was a constant and the emitted sw.js carried no build-varying bytes — its content is a pure function of packages/client/src/sw/**. A deploy that changed only app chunks therefore left the worker byte-identical, the browser found no update, install never re-fired, and precacheAppShell never re-ran. The offline navigation fallback served the first-installed deploy's index.html indefinitely, and deleteStaleCaches explicitly excluded the live cache so nothing could rotate it.

The fix

  • apps/web/vite.config.ts — the manifest plugin digests every emitted non-.map bundle entry (file name plus bytes, sorted) into a 16-hex-char build id, and the worker pass stamps it in via define.
  • packages/client/src/sw/precache.tsAPP_SHELL_CACHE is now cipherbox-app-shell-<buildId>. A new deploy installs into a fresh cache; deleteStaleCaches on activate evicts every cipherbox-app-shell* except the live one, so redeploys no longer accumulate.

Three things worth calling out:

The id is content-derived, not a timestamp. An unchanged build produces an identical sw.js, verified byte-for-byte across repeated clean rebuilds. That keeps the browser from re-installing the worker on every deploy of identical input.

The digest covers file contents, not just the hashed file names. Vite already content-hashes chunk names, but index.html ships unhashed — a change confined to it (meta tag, CSP, title) would change the shipped shell without changing any chunk hash, and a name-only digest would leave the stale document pinned offline.

The build fails closed if the stamp did not land. closeBundle re-reads the emitted sw.js and throws unless it contains the id. Without that, a future change to how define folds would silently collapse the cache name back to cipherbox-app-shell-dev on every deploy — re-introducing this exact defect with every gate green.

Relationship to #969

#974 had two halves. #969 has since landed the server half — carving /assets/* out of the Caddy SPA fallback so a missing fingerprinted asset returns a real 404 instead of a 200 text/html shell. The two are complementary, and neither makes the other redundant:

  • Without fix(web): add CSP and nosniff to the staging web vhost #969, precacheAppShell could cache index.html under an /assets/*.js URL and then serve it cache-first, indefinitely and silently.
  • Without this PR, the worker's bytes never change, so install never re-runs and the shell stays pinned to the first deploy the user installed. A 404 alone cannot refresh anything.

They also compose correctly: precacheAppShell caches per entry rather than via one atomic addAll, so a 404 during a mid-deploy race costs that one asset instead of dropping the whole shell, and pruning follows the manifest rather than what happened to cache.

Verification

Re-run in full after rebasing onto bd30a0cbb, which pulled 12 commits including #951's byte pipe, #969, and #971.

  • pnpm -r --if-present run typecheck — clean across apps/api, apps/web, packages/client.
  • pnpm -r --if-present run testpackages/client 23 files / 266 tests, apps/api 24 / 178, apps/web 13 / 99, all passing. src/sw/precache.test.ts contributes 17 cases, including the new the build stamp > rotates the shell cache so a redeploy installs fresh and evicts its predecessor. Picked up by the existing root pnpm test gate; no workflow edit needed.
  • pnpm --filter @cipherbox/client run test:browser — 28 Playwright tests passing, including the Service Worker media brokerage suite.
  • pnpm exec eslint . — clean. Rust gates (cargo fmt --all --check, cargo check --workspace --all-targets, cargo clippy --workspace --all-targets -- -D warnings, cargo check -p cipherbox-wasm --target wasm32-unknown-unknown) all clean; this PR touches no Rust but the rebase pulls 12 commits of it.

The build id was exercised against real build:bundle output rather than only in unit tests:

Input Build id Manifest
baseline 02fa4f7dc2b9a119
rebuilt, no change 02fa4f7dc2b9a119 identical
index.html title only 079f68eaf4fbaf9d byte-identical file list
a .tsx source literal a4faf30232029cb1 chunk hashes moved
reverted to baseline 02fa4f7dc2b9a119 identical

The index.html row is the load-bearing one: every hashed asset name was unchanged, so a name-only digest would have produced the same id and left the shell pinned. The id moved anyway, which is what proves the digest reads contents.

The new test is non-circular: reverting APP_SHELL_CACHE to the bare constant fails exactly that one case with expected 'cipherbox-app-shell' not to be 'cipherbox-app-shell', leaving the other 16 green. The fail-closed check is live rather than decorative: removing the define makes the build exit non-zero with Error: sw.js did not take the build stamp.

Summary by CodeRabbit

  • New Features

    • Improved app shell caching with build-specific cache versions.
    • Automatically refreshes cached assets when a new build is available.
    • Removes outdated cached assets while retaining the latest version.
  • Bug Fixes

    • Improved coordination between the service worker and generated asset manifest.
    • Added validation to ensure the service worker uses the correct build version.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The Vite build now generates a shared build ID for the app-shell manifest and Service Worker. The Service Worker uses this ID in its cache name, allowing successive deployments to remove stale shell caches. Tests cover cache rotation and build-specific precaching.

Changes

App-shell cache rotation

Layer / File(s) Summary
Build-specific cache contract and validation
packages/client/src/sw/precache.ts, packages/client/src/sw/precache.test.ts
APP_SHELL_CACHE now includes the stamped build ID, with dev as the fallback. Tests verify distinct cache names, successive precaching, predecessor removal, and retention of the latest URLs.
Combined manifest and Service Worker build
apps/web/vite.config.ts
appShell() hashes emitted non-map assets, generates the sorted manifest, passes the build ID to the Service Worker build, and verifies the emitted worker contains the stamp. The worker remains an unhashed root-level IIFE.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Vite as Vite build
  participant AppShell as appShell()
  participant Manifest as precache manifest
  participant ServiceWorker as Service Worker
  participant CacheStorage as Cache Storage
  Vite->>AppShell: emit assets and derive build ID
  AppShell->>Manifest: generate stamped precache manifest
  AppShell->>ServiceWorker: build worker with shared build ID
  ServiceWorker->>CacheStorage: use build-specific app-shell cache
  CacheStorage-->>ServiceWorker: remove predecessor shell cache
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: deriving a build ID to rotate the app-shell cache.
Linked Issues check ✅ Passed The changes stamp the Service Worker, derive build-specific cache names, rotate stale caches, and verify the stamp, satisfying issue #974.
Out of Scope Changes check ✅ Passed All changed files support build-ID stamping, app-shell cache rotation, stale-cache cleanup, or related tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/974-rotate-app-shell-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@FSM1

FSM1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Correction to this PR's "deliberately left out" note: #974's optional second item — carving /assets/* out of the Caddy SPA fallback — is already implemented in #969, not outstanding.

docker/Caddyfile there splits the vhost into handle /assets/* { file_server } and handle { try_files {path} /index.html; file_server }, so a missing fingerprinted asset returns a real 404 instead of the 200 text/html shell that cache.addAll would otherwise cache under a .js URL. That PR's own verification table records the before/after for /assets/MISSING-deadbeef.js, .wasm, and a nested path.

So the two halves of #974's fix direction land in separate PRs — the build-id cache key here, the fallback carve-out in #969 — and Closes #974 on this PR remains correct once both are merged. Nothing is being dropped, and no follow-up issue is needed.

@FSM1
FSM1 force-pushed the fix/974-rotate-app-shell-cache branch from 1969997 to 19d85a9 Compare August 3, 2026 19:34
The Service Worker's cache name was a constant and `sw.js` carried no
build-varying bytes, so a deploy that changed only app chunks left the
worker byte-identical: no update check fired, `install` never re-ran, and
the offline navigation fallback pinned the first deploy a user installed.
`deleteStaleCaches` excluded the live cache, so nothing could rotate it.

The web build now digests every emitted chunk into a build id, stamps it
into the worker via `define`, and the shell cache is keyed off it. A new
deploy therefore installs into a fresh cache and `activate` evicts the one
it superseded. The id is derived from the output bytes, not a clock, so an
unchanged build keeps its id.

The digest covers file contents rather than the hashed file names alone:
`index.html` ships unhashed, so a change confined to it would otherwise
leave the shell pinned. The build fails closed if the emitted worker did
not take the stamp, since an unstamped worker silently stops rotating.

Closes #974
@FSM1
FSM1 force-pushed the fix/974-rotate-app-shell-cache branch from 19d85a9 to 60b6732 Compare August 3, 2026 23:13
@FSM1
FSM1 marked this pull request as ready for review August 3, 2026 23:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/vite.config.ts`:
- Around line 1-3: Move the appShell build-fingerprinting logic and its SHA-256
hashing helper out of apps/web into an approved build-tool module, including the
related crypto, filesystem, and path imports. Update vite.config.ts to consume
the relocated appShell implementation only, preserving its existing
configuration behavior without retaining cryptography or other non-UI TypeScript
there.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8554889d-fb04-405a-980b-5f52cb7b5cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 553ba3b and 60b6732.

📒 Files selected for processing (3)
  • apps/web/vite.config.ts
  • packages/client/src/sw/precache.test.ts
  • packages/client/src/sw/precache.ts

Comment thread apps/web/vite.config.ts
@FSM1
FSM1 merged commit bf8bc74 into main Aug 3, 2026
24 checks passed
@FSM1
FSM1 deleted the fix/974-rotate-app-shell-cache branch August 3, 2026 23:28
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.

The app-shell cache is never rotated so the offline shell pins the first installed deploy

1 participant