# Journey: PWA update flow

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

- [`apps/oshun/web/e2e/pwa-install-update-offline.spec.ts`](../../apps/oshun/web/e2e/pwa-install-update-offline.spec.ts)
  covers the waiting-worker prompt, apply, controller change, cache transition,
  route restoration, and reduced-motion presentation.
- [`apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.ts`](../../apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.ts)
  covers reading-context restoration, consent gating, broadcast coordination,
  cold-start recovery, and applied telemetry.
- [`apps/oshun/web/e2e/pwa-failure-modes.spec.ts`](../../apps/oshun/web/e2e/pwa-failure-modes.spec.ts)
  covers unsupported/error/controller edge states.
- [`apps/oshun/web/e2e/pwa-smoke.spec.ts`](../../apps/oshun/web/e2e/pwa-smoke.spec.ts)
  protects the registration and prompt smoke path.
- [`apps/oshun/web/e2e/offline-background-sync.spec.ts`](../../apps/oshun/web/e2e/offline-background-sync.spec.ts)
  proves 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`](../shell/03-pwa-behavior.md) — registration,
  update, caching, relaunch, and sync policy.
- [`shell/01-app-shell.md`](../shell/01-app-shell.md) — bootstrap placement.
- [`shell/06-keyboard-a11y.md`](../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`](./install-as-pwa.md) — registration/install path before
  updates can exist.
- [`offline-first-time-use.md`](./offline-first-time-use.md) — what is actually
  available from warmed caches.
- [`messages-quiet-hours-and-channel-binding.md`](./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?
