Skip to content

Offline-first Step 1: IndexedDB read cache + installable PWA (#68)#111

Merged
thetechjon merged 1 commit into
devfrom
feat/offline-pwa-step1-68
Jun 6, 2026
Merged

Offline-first Step 1: IndexedDB read cache + installable PWA (#68)#111
thetechjon merged 1 commit into
devfrom
feat/offline-pwa-step1-68

Conversation

@thetechjon
Copy link
Copy Markdown
Collaborator

Closes Step 1 of #68. Step 2 (offline edit queue) tracked in #110.

What's cached (read-only, on a per-repo basis)

Surface Where Hydrates on boot?
Note bodies + folder tree noteser-notes:<owner>/<name> and noteser-folders:<owner>/<name> (Zustand persist → idbStorage) Yes — existing behaviour
Workspace (open tabs) noteser-workspace (existing) Yes
Attachments already viewed noteser-attachment:<path> (existing) Yes
Per-repo snapshot (NEW) noteser-vault-cache:<owner>/<name> Cold-read on boot for the offline status badge
ETag-conditioned blob/tree responses noteser:gh-etag:* (from PR #107) Lazy — survives reload, sends If-None-Match
App shell (HTML/JS/CSS/fonts/icons) Service worker noteser-shell-<build> Cache-first

What's NOT cached (intentionally — Step 2 territory)

How invalidation works

SHA-driven. After every successful pullFromGitHub, the snapshot is rewritten with { commitSha, treeMap, syncedAt }. The next pull compares HEAD against snapshot.commitSha; a match means our cached vault is consistent. clearVaultSnapshot(repo) drops it explicitly (used by "Discard local changes" and tests); a Wipe vault drops it via the existing noteser- prefix walker in reset.ts.

Core vs plugin (per [[feedback-noteser-features-via-plugins]])

This is core, not a plugin, because:

  1. The cache is a property of the storage substrate every user depends on. Every note in the app reads through useNoteStore, which persists via idbStorage. There is no surface a plugin could attach to that wouldn't itself need the cache the moment it touches a note body.
  2. Offline-first is the Fix notes/folders persistence #1 competitive gap in docs/competitive-analysis.md. Punting it to a plugin would mean the default app remains "online-only" — directly contradicting the gap analysis and the install-banner copy ("Install noteser for offline use").
  3. The SW + manifest already live in core (merged before this PR). Moving the read cache into a plugin would split offline behaviour across two opt-in surfaces — a confusing UX where the manifest claims offline but the cache layer is opt-in.

A plugin still makes sense for any richer offline behaviour — selective cache eviction policies, full-text reindex on cache hit, custom conflict UX — those are user-pickable and can layer on top of this substrate without changing it.

Verify

Locally on this branch:

  • npm run lint — clean.
  • npx tsc --noEmit — zero errors.
  • npm test -- --ci — 196 suites passing (2405 tests), 17 skipped, 0 failing.
  • npm run build — production build green.
  • Manual smoke: DevTools → Application: manifest detected with correct icons + theme. SW registered as noteser-shell-<buildid> and intercepts navigation. Offline mode in DevTools → cached vault loads, status shows "Offline — using cached vault" instead of the red error toast.

Tests added

  • src/__tests__/vaultSnapshotCache.test.ts — round-trip, per-repo isolation, SHA-change invalidation, corrupt-entry guard, key-prefix lock.
  • src/__tests__/syncPullWritesSnapshot.test.tspullFromGitHub records the commit SHA + tree map; subsequent pulls overwrite.
  • src/__tests__/pwaProvider.test.tsx — mounts cleanly + renders null until an event fires. The production-only SW registration side-effect is flagged in the test header as intentionally not covered (NODE_ENV mid-run swap is too brittle for the value — pwaManifest.test.ts + the manual Devtools smoke cover the wire shape).

Files changed

New:

  • docs/offline-pwa-plan.md
  • src/utils/vaultSnapshotCache.ts
  • src/hooks/useVaultCacheStatus.ts
  • 3× test files

Touched:

  • src/utils/githubSync/syncPull.ts (writes the snapshot after classify)
  • src/hooks/useAutoSync.ts (offline short-circuit + online listener)
  • src/hooks/useGitHubSync.ts (quieter offline error path)
  • docs/sync.md, docs/user-guide.md (Offline sections)

🤖 Generated with Claude Code

The PWA manifest, service worker, and install banner already live on dev
(merged separately). Step 1 of issue #68 adds the missing piece — a
per-repo snapshot that anchors the cached vault to a real commit SHA, so
the boot path can detect cache hits, surface a "cached at <sha>" status,
and short-circuit a useless network round-trip when the browser is
offline.

What's new:
- docs/offline-pwa-plan.md — design doc covering what gets cached, SHA-
  driven invalidation, the file map for this PR, and the deferred Step 2
  edit-queue shape so the next PR can land without re-litigating it.
- src/utils/vaultSnapshotCache.ts — read/write/clear the per-repo
  snapshot under `noteser-vault-cache:<owner>/<name>`. The key uses the
  same dash-prefix convention as `noteser-attachment:` so reset.ts wipes
  it on a "Wipe vault".
- src/hooks/useVaultCacheStatus.ts — exposes { isOffline, snapshot } for
  any UI surface that wants to render "Offline — cached at abc1234".
- syncPull.ts writes the snapshot after a successful classify (fire-and-
  forget so a quota error never fails the sync).
- useAutoSync short-circuits the startup pull when navigator.onLine ===
  false and registers an `online` listener so connectivity recovery
  triggers an auto-pull (5s debounce to handle flapping connections).
- useGitHubSync.runPullOnly swaps the red "Pull failed" toast for a
  quiet "Offline — using cached vault" when the failure is a network
  error AND the browser is offline. Auth / 5xx still surface normally.
- docs/sync.md + docs/user-guide.md — short Offline sections pointing
  at the plan doc.

What's NOT in this PR (deferred to Step 2 follow-up):
- The offline edit queue + reconciliation on reconnect. Push stays
  fully online; offline edits live in the local stores as today and
  need a manual Sync click after coming back online.

Tests:
- vaultSnapshotCache.test.ts — round-trip, per-repo isolation, SHA-change
  invalidation, corrupt-entry guard, key-prefix lock.
- syncPullWritesSnapshot.test.ts — pullFromGitHub records the commit SHA
  + tree map under the per-repo key; subsequent pulls overwrite cleanly.
- pwaProvider.test.tsx — mounts cleanly + renders null until an event
  fires. The production-only SW.register side-effect is flagged as
  intentionally not covered (NODE_ENV mid-run swap is too brittle —
  pwaManifest.test.ts + manual Devtools smoke cover the wire shape).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@thetechjon thetechjon merged commit c07e154 into dev Jun 6, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant