fix: key the app-shell cache off a content-derived build id - #979
Conversation
WalkthroughThe 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. ChangesApp-shell cache rotation
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Correction to this PR's "deliberately left out" note: #974's optional second item — carving
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 |
1969997 to
19d85a9
Compare
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
19d85a9 to
60b6732
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
apps/web/vite.config.tspackages/client/src/sw/precache.test.tspackages/client/src/sw/precache.ts
Closes #974
The defect
APP_SHELL_CACHEwas a constant and the emittedsw.jscarried no build-varying bytes — its content is a pure function ofpackages/client/src/sw/**. A deploy that changed only app chunks therefore left the worker byte-identical, the browser found no update,installnever re-fired, andprecacheAppShellnever re-ran. The offline navigation fallback served the first-installed deploy'sindex.htmlindefinitely, anddeleteStaleCachesexplicitly excluded the live cache so nothing could rotate it.The fix
apps/web/vite.config.ts— the manifest plugin digests every emitted non-.mapbundle entry (file name plus bytes, sorted) into a 16-hex-char build id, and the worker pass stamps it in viadefine.packages/client/src/sw/precache.ts—APP_SHELL_CACHEis nowcipherbox-app-shell-<buildId>. A new deploy installs into a fresh cache;deleteStaleCachesonactivateevicts everycipherbox-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.htmlships 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.
closeBundlere-reads the emittedsw.jsand throws unless it contains the id. Without that, a future change to howdefinefolds would silently collapse the cache name back tocipherbox-app-shell-devon 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 a200 text/htmlshell. The two are complementary, and neither makes the other redundant:precacheAppShellcould cacheindex.htmlunder an/assets/*.jsURL and then serve it cache-first, indefinitely and silently.installnever re-runs and the shell stays pinned to the first deploy the user installed. A 404 alone cannot refresh anything.They also compose correctly:
precacheAppShellcaches per entry rather than via one atomicaddAll, 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 acrossapps/api,apps/web,packages/client.pnpm -r --if-present run test—packages/client23 files / 266 tests,apps/api24 / 178,apps/web13 / 99, all passing.src/sw/precache.test.tscontributes 17 cases, including the newthe build stamp > rotates the shell cache so a redeploy installs fresh and evicts its predecessor. Picked up by the existing rootpnpm testgate; 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:bundleoutput rather than only in unit tests:02fa4f7dc2b9a11902fa4f7dc2b9a119index.htmltitle only079f68eaf4fbaf9d.tsxsource literala4faf30232029cb102fa4f7dc2b9a119The
index.htmlrow 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_CACHEto the bare constant fails exactly that one case withexpected 'cipherbox-app-shell' not to be 'cipherbox-app-shell', leaving the other 16 green. The fail-closed check is live rather than decorative: removing thedefinemakes the build exit non-zero withError: sw.js did not take the build stamp.Summary by CodeRabbit
New Features
Bug Fixes