Repository navigation
fix: never let a blocked service worker job queue stop the app from booting - #22
Merged
Merged
Conversation
…ooting A QA report of a feature preview stuck at "2% Registering service worker", through reloads, with no error: `navigator.serviceWorker.register()` had neither resolved nor rejected, and the bootstrap pipeline waited on it with no deadline. Service worker jobs are serialised per scope. An install that never settles holds the queue, and every later `register()`, `unregister()` and `getRegistrations()` on that origin stays pending behind it until the browser times the install out minutes later. Reproduced end to end: with a worker whose install hangs, the next visit's `register()` never settles and `unregister()` hangs with it — so the loading widget's own reset button, which awaited `unregister()` before reloading, hung exactly when it was needed. What produces such an install here is the precache: `fetchWithRetry` bounds the response headers and clears its abort timer the moment they arrive, so `cache.put` then reads a body — `main.dart.js` is routinely ~10 MB — with no deadline at all. Three deadlines, one per hop: - The whole SW step of the pipeline is capped (`SW_BOOTSTRAP_TIMEOUT_MS`, 10s) and the app boots uncached if it expires. The abandoned registration keeps running, so the worker still installs once the browser frees the queue. This is not a new state: the pipeline already proceeds without a SW whenever activation times out. - Precached bodies get an idle watchdog (`BODY_STALL_TIMEOUT_MS`, 30s) that cancels a stream which has stopped producing bytes. Idle time, not total duration, so a slow-but-alive connection still finishes however long it takes; a dead one fails one install attempt instead of blocking the origin. - The reset action is bounded (`RESET_TIMEOUT_MS`, 3s) and always reloads. Making the reset reachable also made its blast radius reachable, so it is narrowed to the caching layer it is named for: application storage is no longer cleared (on Flutter web `shared_preferences` is `localStorage`, so clearing it signed the user out), and only the registration this bootstrap owns is removed — unregistering everything also dropped push subscriptions belonging to workers at unrelated scopes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Qkiw3K6ozvtWNHCTmFiAK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happened
QA could not open a feature preview: the page sat at
2% Registering service workerforever, across reloads. The console stopped mid-registration — noRegistered successfully, noRegistration failed— sonavigator.serviceWorker.register()had neither resolved nor rejected, and the bootstrap pipeline awaited it with no deadline.Root cause
Service worker jobs are serialised per scope. An install that never settles holds the queue, and every later
register(),unregister()andgetRegistrations()on that origin stays pending behind it until the browser times the install out — minutes later.Reproduced end to end against a worker whose install hangs:
register()installingregister()unregister()caches.delete()What produces such an install here is the precache.
fetchWithRetrybounds the response headers and clears its abort timer the moment they arrive;cache.putthen reads the body —main.dart.jsis routinely ~10 MB — with no deadline at all. A stream that dies mid-transfer hangs the install, and with it the whole origin.The loading widget's reset button could not rescue this either: it awaited
unregister()before reloading, so it hung exactly when it was needed.The fix
Three deadlines, one per hop:
SW_BOOTSTRAP_TIMEOUT_MS(10s) caps the whole service-worker step of the pipeline; the app boots uncached if it expires. The abandoned registration is left running, so the worker still installs once the browser frees the queue. Not a new state — the pipeline already proceeds without a SW whenever activation times out.BODY_STALL_TIMEOUT_MS(30s) is an idle watchdog on precached bodies: a stream that stops producing bytes is cancelled. Idle time, not total duration, so a slow-but-alive connection still finishes however long it takes. A dead one fails a single install attempt, retried on the next page load.RESET_TIMEOUT_MS(3s) bounds the reset action, which now always reloads.Making the reset reachable also made its blast radius reachable, so it is narrowed to the caching layer it is named for:
shared_preferencesislocalStorage, so clearing it signed the user out and dropped local state;reloadIfForeignControllerhas always been careful to spare.Verification
flutter-viewnever attached — the QA symptom, in a real browser.npm run verify: committed Dart assets match the build output.Local e2e ran against the system Chrome: this machine cannot finish a
playwright install(download completes, extraction stalls). CI installs its own browsers and is unaffected.Release
pubspec.yaml→ 0.2.0, CHANGELOG updated. README,docs/service-worker.mdanddocs/bootstrap-pipeline.mddocument the new deadlines — notedocs/service-worker.mdpreviously claimed a 30s install timeout that did not exist.🤖 Generated with Claude Code
https://claude.ai/code/session_015Qkiw3K6ozvtWNHCTmFiAK