Offline editing: local persistence, PWA shell, and reconnect UX #38

Closed
opened 2026-07-04 14:52:17 +02:00 by fable-5 · 1 comment
Collaborator

Context

Kickoff decision: continue working without a connection; merge conflict-free on reconnect (ADR 0003).

Scope

Add y-indexeddb to the editor document hook (local persistence for every opened page), vite-plugin-pwa service worker caching the app shell, offline UI state (banner + which page versions are local-only), reconnect flow (provider resync merges automatically), and cleanup of local state after successful sync when leaving the page.

Acceptance criteria

  • kill the network, edit, reload the tab — content is there from IndexedDB; restore network — server and second client converge (e2e)
  • app shell loads offline for a previously visited page (service worker test)
  • IndexedDB does not grow unboundedly (cleanup verified)
  • no service-worker cache poisoning of API responses (only static assets cached)

Technical notes

  • ADR 0003, realtime-collaboration.md §Offline (incl. the out-of-scope note: no offline page creation).

Dependencies

Depends on #36.

Size: ~2 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Kickoff decision: continue working without a connection; merge conflict-free on reconnect (ADR 0003). ## Scope Add `y-indexeddb` to the editor document hook (local persistence for every opened page), `vite-plugin-pwa` service worker caching the app shell, offline UI state (banner + which page versions are local-only), reconnect flow (provider resync merges automatically), and cleanup of local state after successful sync when leaving the page. ## Acceptance criteria - [ ] kill the network, edit, reload the tab — content is there from IndexedDB; restore network — server and second client converge (e2e) - [ ] app shell loads offline for a previously visited page (service worker test) - [ ] IndexedDB does not grow unboundedly (cleanup verified) - [ ] no service-worker cache poisoning of API responses (only static assets cached) ## Technical notes - ADR 0003, realtime-collaboration.md §Offline (incl. the out-of-scope note: no offline page creation). ## Dependencies Depends on #36. **Size**: ~2 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M3 — Real-time collaboration & history milestone 2026-07-04 14:52:17 +02:00
fable-5 added the
frontend
label 2026-07-04 14:52:17 +02:00
Collaborator

Verified on Test and Int (commit 7b17e05, pipeline green — all CI + CD jobs, incl. the new offline e2e pack, and Int promotion).

Live on Test: the PWA is deployed — GET /sw.js (200) contains precacheAndRoute (app-shell precache), a NavigationRoute fallback, and the /api + /collab denylist; registerSW.js is referenced from index.html. The full offline-reload behaviour is browser-only and is covered by the green CI offline pack against a real api+collab+web stack.

What landed (ADR 0003, realtime-collaboration.md §Offline):

  • y-indexeddb mirrors every opened page's Y.Doc to IndexedDB (shared with the collab provider). The local copy is discarded when the page is left after a successful server sync (bounding growth) and kept otherwise so offline edits survive to the next visit.
  • vite-plugin-pwa service worker precaches the app shell (build assets only) with a navigation fallback; /api and /collab are denylisted and there is no runtime caching, so API responses are never cached or poisoned (the offline e2e asserts an API call fails offline rather than serving a stale 200).
  • Offline page resolution without caching API (option 1): the app itself persists the small metadata it needs to reopen a visited page (page/pond ids + slugs, bounded-LRU localStorage) and the last signed-in user, so after an offline tab reload it stays signed in, resolves the page, and restores content from IndexedDB. Both revalidate on reconnect (a 401 clears the cached user).
  • Local-only UI banner when edits are held only on this device (provider onUnsyncedChanges); de + en.

Tests: page-cache unit test (remember/recall + bounded eviction); the offline e2e pack — edit, reload while offline (shell from the SW, content from IndexedDB), assert no SW API caching, reconnect, second client converges.

CI debugging note (for the record): the offline pack first failed CI-only. The runner log showed the e2e static web server had crashed (ECONNREFUSED :5173) — its /collab WebSocket proxy didn't handle error on the client socket, so an abruptly-dropped WS (Linux/Node 22 emits error; local macOS/Node 26 emitted close, which is why it passed locally) raised an uncaught exception and took the test server down between packs. Fixed by handling the client-socket error + an uncaughtException guard on that throwaway server.

Minor follow-up (non-blocking): the deployed web container serves manifest.webmanifest as application/octet-stream; harmless for the SW, but its mime could be set to application/manifest+json for clean PWA installability.

Verified on Test and Int (commit 7b17e05, pipeline green — all CI + CD jobs, incl. the new offline e2e pack, and Int promotion). **Live on Test:** the PWA is deployed — `GET /sw.js` (200) contains `precacheAndRoute` (app-shell precache), a `NavigationRoute` fallback, and the `/api` + `/collab` denylist; `registerSW.js` is referenced from `index.html`. The full offline-reload behaviour is browser-only and is covered by the green CI `offline` pack against a real api+collab+web stack. **What landed (ADR 0003, realtime-collaboration.md §Offline):** - **y-indexeddb** mirrors every opened page's `Y.Doc` to IndexedDB (shared with the collab provider). The local copy is discarded when the page is left after a successful server sync (bounding growth) and kept otherwise so offline edits survive to the next visit. - **vite-plugin-pwa** service worker precaches the app shell (build assets only) with a navigation fallback; `/api` and `/collab` are denylisted and there is no runtime caching, so API responses are never cached or poisoned (the offline e2e asserts an API call fails offline rather than serving a stale 200). - **Offline page resolution without caching API** (option 1): the app itself persists the small metadata it needs to reopen a visited page (page/pond ids + slugs, bounded-LRU localStorage) and the last signed-in user, so after an offline tab reload it stays signed in, resolves the page, and restores content from IndexedDB. Both revalidate on reconnect (a 401 clears the cached user). - **Local-only UI** banner when edits are held only on this device (provider `onUnsyncedChanges`); de + en. **Tests:** `page-cache` unit test (remember/recall + bounded eviction); the `offline` e2e pack — edit, reload while offline (shell from the SW, content from IndexedDB), assert no SW API caching, reconnect, second client converges. **CI debugging note (for the record):** the offline pack first failed CI-only. The runner log showed the e2e static web server had crashed (`ECONNREFUSED :5173`) — its `/collab` WebSocket proxy didn't handle `error` on the client socket, so an abruptly-dropped WS (Linux/Node 22 emits `error`; local macOS/Node 26 emitted `close`, which is why it passed locally) raised an uncaught exception and took the test server down between packs. Fixed by handling the client-socket error + an `uncaughtException` guard on that throwaway server. **Minor follow-up (non-blocking):** the deployed web container serves `manifest.webmanifest` as `application/octet-stream`; harmless for the SW, but its mime could be set to `application/manifest+json` for clean PWA installability.
Sign in to join this conversation.
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#38
No description provided.