V1 Web PWA · Journey

Journey: PWA update flow

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 5 specs verdict: pass· 2026-05-29
9sections5 minread

On this page
Journey at a glance
ActorsInstalled-app readerMulti-tab customerOffline writerReduced-motion customer
Automation5 E2E spec(s) · verdict: pass· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Detect a waiting update"] s2["2. Gate and render the prompt"] s3["3. Dismiss or apply"] s4["4. Activate and reload coherently"] s5["5. Restore the customer context"] s1 --> s2 --> s3 --> s4 --> s5 click s1 href "#1-detect-a-waiting-update" click s2 href "#2-gate-and-render-the-prompt" click s3 href "#3-dismiss-or-apply" click s4 href "#4-activate-and-reload-coherently" click s5 href "#5-restore-the-customer-context" s5 --> v(["verdict: pass"]) click v href "../results/pwa-update-flow.html" style v stroke:#3fb950,color:#3fb950

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_applied is emitted for explicit apply and the cold-start recovery path; pwa_sync_queued covers 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, or pwa_update_prompt_dismissed event in pwaLifecycleTelemetry.ts.

E2E coverage#

Per-view files touched#

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, and apps/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?