01-app-shell.md03-pwa-behavior.md06-keyboard-a11y.mdJourney flow#
Generated from the authored steps below — click a node to jump to that section.
This is the installed-app update contract from a waiting service worker to a controlled reload. The shipped experience protects reading context, coordinates sibling tabs, preserves queued-write messaging, and degrades safely when no waiting worker remains. It emits applied/sync telemetry only; the often-claimed available, prompt-shown, and prompt-dismissed events do not exist.
Personas#
- Installed-app reader — receives a low-interruption update prompt while reading and expects the route and reading position to survive.
- Multi-tab customer — applies an update in one tab without leaving sibling tabs on an incompatible controller.
- Offline writer — needs to know that explicitly queued writes remain queued while app code updates.
- Reduced-motion customer — receives the same state changes without the decorative transition.
Pre-conditions#
- The browser supports service workers and the Oshun worker is registered by
PwaBootstrap. - A new worker has reached
waiting, or the active worker broadcasts an update message that resolves to the same update token. - Cookie consent has resolved to accepted or rejected. While consent is still
null, the prompt is deliberately withheld. - The current route may be an ordinary shell surface or a reading context; the latter selects the quieter reading-specific prompt copy and placement.
Steps#
1. Detect a waiting update#
PwaBootstrap listens to registration state, worker messages, and controller
changes. A waiting worker creates a stable pending-update token. A controller
change without an explicit apply intent does not hard-reload the page; it moves
the prompt into a ready state so the browser does not yank an active reading
session.
2. Gate and render the prompt#
The prompt renders only when pendingUpdate exists, its token has not been
dismissed in this component lifetime, and consent has resolved. Rejected cookie
consent counts as resolved. The component uses role="status" and
aria-live="polite", adapts its placement for reading contexts, and includes
the current explicit background-sync queue count when one exists.
3. Dismiss or apply#
Dismissal suppresses only the current token in component state; it is not a
durable preference and a future token may prompt again. Apply stores the current
route and, for supported reading surfaces, scroll position. It broadcasts an
apply intent to sibling tabs, sends OSHUN_SKIP_WAITING to the waiting worker,
and enters an applying state.
4. Activate and reload coherently#
On activation the worker removes stale Oshun caches while preserving the current
cache generation and unrelated caches. The initiating tab reloads on
controllerchange; sibling tabs that received the apply intent do the same. The
component has bounded fallback reloads (two seconds in the prompt and four
seconds in the bootstrap) so an unusual controller transition does not strand
the customer.
5. Restore the customer context#
The browser returns to the stored route rather than a generic home page. Reading scroll restoration is bounded to a valid document position. Queued background writes remain governed by their own queue/drain policy; updating the shell does not pretend those writes have synced.
Post-conditions#
- All cooperating tabs run under the new active worker after a bounded reload.
- The initiating route is preserved and supported reading positions are restored.
- Stale Oshun caches are purged without deleting foreign caches or the new generation.
pwa_update_appliedis emitted for explicit apply and the cold-start recovery path;pwa_sync_queuedcovers explicit queued writes.
Failure modes#
- No service-worker support — registration is skipped and no false update prompt appears.
- Consent unresolved — the pending token is retained but the prompt stays hidden until consent resolves.
- Waiting worker disappears — apply falls back to a normal bounded reload; it cannot message a worker that no longer exists.
- Unsolicited controller change — does not immediately reload the reader.
- Dismiss then remount — dismissal is not durable; a remount may show the same waiting update again.
- Queued writes present — copy reports the queue, but the update does not drain arbitrary failed requests. Only writes explicitly admitted to the service-worker queue participate in background sync.
- Telemetry overclaim — there is no
pwa_update_available,pwa_update_prompt_shown, orpwa_update_prompt_dismissedevent inpwaLifecycleTelemetry.ts.
E2E coverage#
apps/oshun/web/e2e/pwa-install-update-offline.spec.tscovers the waiting-worker prompt, apply, controller change, cache transition, route restoration, and reduced-motion presentation.apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.tscovers reading-context restoration, consent gating, broadcast coordination, cold-start recovery, and applied telemetry.apps/oshun/web/e2e/pwa-failure-modes.spec.tscovers unsupported/error/controller edge states.apps/oshun/web/e2e/pwa-smoke.spec.tsprotects the registration and prompt smoke path.apps/oshun/web/e2e/offline-background-sync.spec.tsproves the separate explicit-write queue and sync telemetry seam.- Coverage depth: deep for the in-browser lifecycle; browser/vendor update scheduling remains outside deterministic app control.
Per-view files touched#
shell/03-pwa-behavior.md— registration, update, caching, relaunch, and sync policy.shell/01-app-shell.md— bootstrap placement.shell/06-keyboard-a11y.md— polite status and reduced-motion behavior.- Relevant reading views inherit the same prompt through the shared bootstrap; they do not implement their own updater.
Cross-references#
install-as-pwa.md— registration/install path before updates can exist.offline-first-time-use.md— what is actually available from warmed caches.messages-quiet-hours-and-channel-binding.md— unrelated notification preference lifecycle.- Sources:
apps/oshun/web/src/components/pwa/PwaBootstrap.tsx,apps/oshun/web/src/components/pwa/PwaUpdatePrompt.tsx,apps/oshun/web/src/lib/pwa-service-worker.ts, andapps/oshun/web/src/analytics/pwaLifecycleTelemetry.ts.
Open questions#
- Should dismissing an update survive a component remount or browser restart, and if so for how long?
- Should prompt shown/dismissed telemetry be added, with consent-safe semantics, or should the docs continue to treat only apply as observable?
- Which additional reading surfaces should persist a domain-specific resume position instead of route-only continuity?