Skip to content

fix: register the Semaphore service worker when hydration finishes after load - #242

Merged
soamdesai-tfh merged 2 commits into
mainfrom
soam/confident-stonebraker-e5953e
Sep 14, 2026
Merged

soamdesai-tfh merged 2 commits into
mainfrom
soam/confident-stonebraker-e5953e

Conversation

@soamdesai-tfh

Copy link
Copy Markdown
Contributor

Summary

The Semaphore service worker was never registered in next dev, and could be skipped in production on slow hydration.

src/pages/_app.tsx registered /sw.js from an inline next/script tag. With the default afterInteractive strategy that script runs after React hydration and only attaches a window load listener. When hydration finishes after load has already fired, the listener never runs, so:

  • /sw.js is never registered and /semaphore/semaphore.wasm + .zkey are never cached,
  • retryDownload() in src/lib/utils.ts hangs on navigator.serviceWorker.ready,
  • the "Downloading Semaphore" chip never clears.

Changes

  • src/lib/register-service-worker.ts (new): registers immediately when document.readyState === "complete", otherwise waits for load with a once listener, and returns a cleanup that removes the pending listener. Registration failures are logged with the script URL and consequence, never swallowed.
  • src/pages/_app.tsx: calls the helper from a dedicated effect and drops the inline <Script>. No inline script means no CSP nonce is needed for registration. getInitialProps / pageProps.nonce are intentionally left in place: next build reports that this getInitialProps opts every route out of Automatic Static Optimization, so removing it is a separate decision.
  • tests/register-service-worker.test.ts (new): five node:test cases covering the immediate branch, the deferred branch, cleanup, the rejection path, and browsers without service worker support.
  • .eslintrc.json: "root": true, in its own commit. Checkouts nested inside another copy of the repo made ESLint cascade into the parent config and fail on a conflicting tailwindcss plugin. No effect in CI; easy to drop if unwanted.

Verification

Before the fix, in next dev with public/sw.js generated: page hydrated, document.readyState === "complete", inline #sw script element present, getRegistrations() empty, no caches, chip stuck.

After the fix, from a clean state (worker unregistered, caches deleted):

Check next dev next build + next start
navigator.serviceWorker.getRegistrations() 1 registration, /sw.js, activated 1 registration, /sw.js, activated
semaphore-cache contents wasm + zkey wasm + zkey
"Downloading Semaphore" chip gone gone
Console errors / CSP violations none none; CSP header enforced, 0 inline scripts without a nonce

pnpm lint, pnpm typecheck, pnpm test (25/25), pnpm spellcheck and pnpm build all pass.

Notes

  • public/sw.js is gitignored and generated by pnpm build; for dev, run pnpm exec workbox injectManifest workbox-config.cjs first.
  • Pre-existing and not addressed here: public/service-worker.js configures ExpirationPlugin({ maxEntries: 1 }) on a cache that must hold two files, so one entry is likely evicted after the first proof and re-downloaded on later visits.

🤖 Generated with Claude Code

soamdesai-tfh and others added 2 commits September 14, 2026 15:45
…ter load

The registration lived in an inline next/script tag with the default
afterInteractive strategy, so it ran after React hydration and only added
a window "load" listener. Whenever hydration finished after "load" had
already fired (always in `next dev`, and in production on slow devices)
the listener never ran: /sw.js was never registered, the Semaphore wasm
and zkey were never cached, retryDownload() hung on
navigator.serviceWorker.ready and the "Downloading Semaphore" chip never
cleared.

Move registration into a React effect via a small helper that registers
immediately when document.readyState is already "complete" and otherwise
waits for "load", removing the listener on cleanup. Registration
failures are logged with the script URL and consequence. Dropping the
inline script also removes the CSP nonce dependency for this code path.

Add node tests for both branches, cleanup, the rejection path and
browsers without service worker support.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Checkouts nested inside another copy of the repo (for example under
.claude/worktrees) made ESLint cascade into the parent's config and fail
with a conflicting "tailwindcss" plugin. No effect where no parent config
exists, such as CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
worldcoin-simulator Ready Ready Preview Sep 14, 2026 10:46pm UTC

Request Review

@soamdesai-tfh
soamdesai-tfh merged commit 66e28d2 into main Sep 14, 2026
10 checks passed
@soamdesai-tfh
soamdesai-tfh deleted the soam/confident-stonebraker-e5953e branch September 14, 2026 22:46

This branch was successfully deployed

1 active deployment
Preview — 96fc9c2c Deployed Sep 14, 2026 by vercel[bot]
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.

1 participant