Offline-first Step 1: IndexedDB read cache + installable PWA (#68)#111
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes Step 1 of #68. Step 2 (offline edit queue) tracked in #110.
What's cached (read-only, on a per-repo basis)
noteser-notes:<owner>/<name>andnoteser-folders:<owner>/<name>(Zustand persist →idbStorage)noteser-workspace(existing)noteser-attachment:<path>(existing)noteser-vault-cache:<owner>/<name>noteser:gh-etag:*(from PR #107)If-None-Matchnoteser-shell-<build>What's NOT cached (intentionally — Step 2 territory)
/api/git-proxy/*and/api/github/*stay fully online — the SW bypasses them by URL.How invalidation works
SHA-driven. After every successful
pullFromGitHub, the snapshot is rewritten with{ commitSha, treeMap, syncedAt }. The next pull compares HEAD againstsnapshot.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 existingnoteser-prefix walker inreset.ts.Core vs plugin (per [[feedback-noteser-features-via-plugins]])
This is core, not a plugin, because:
useNoteStore, which persists viaidbStorage. There is no surface a plugin could attach to that wouldn't itself need the cache the moment it touches a note body.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").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.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.ts—pullFromGitHubrecords 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.mdsrc/utils/vaultSnapshotCache.tssrc/hooks/useVaultCacheStatus.tsTouched:
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