fix(15.1): prevent logout race in search index init#200
Conversation
When clearIndex fires (on logout) while init() is suspended at await loadEncrypted(), the resumed init would overwrite isIndexReady with true and persist encrypted key material to IndexedDB after the user has logged out. Fix: add cancelledRef that clearIndex sets to true. init() checks this flag after each await and bails out before writing state or persisting. clearIndex also resets buildingRef to prevent deadlock if logout interrupts mid-build. Addresses: #198 (comment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: be0b498729fa
- Todo: async/incremental search index build for large vaults - Learning: run E2E locally before push Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 6f54eaf3475f
WalkthroughThe PR adds documentation on running E2E tests locally, updates the planning state with two new pending tasks (CRDT IPNS inbox sharing and async incremental search indexing), introduces a new planning document detailing strategies for async search index building, and modifies the useSearch hook to track and handle cancellation of in-flight async initialization operations. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/hooks/useSearch.ts (1)
153-163:⚠️ Potential issue | 🟡 Minor
rebuildIndexcallspersistEncryptedwithout checkingcancelledRef.If
triggerSearchIndexRebuildfires just before logout (e.g., from a sync callback),rebuildIndexholds a non-nullvaultKeypairin its closure and callspersistEncryptedwithout any cancellation guard.clearIndexmay run concurrently whilepersistEncrypted's async writes are in flight.The
if (!vaultKeypair) returnguard partially mitigates this (a newrebuildIndexwithvaultKeypair = nullis created after logout), but does not close the narrow window where the old closure is already executing with a captured non-null keypair.🛡️ Proposed fix
const rebuildIndex = useCallback(() => { if (!vaultKeypair) return; + if (cancelledRef.current) return; const currentFolders = useFolderStore.getState().folders; searchIndexService.buildFromFolderTree(currentFolders); + if (cancelledRef.current) return; setIsIndexReady(true); // Re-persist (fire-and-forget) - searchIndexService.persistEncrypted(vaultKeypair.privateKey).catch((err) => { - console.warn('[search] Failed to persist index after rebuild:', err); - }); + if (!cancelledRef.current) { + searchIndexService.persistEncrypted(vaultKeypair.privateKey).catch((err) => { + console.warn('[search] Failed to persist index after rebuild:', err); + }); + } }, [vaultKeypair]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/hooks/useSearch.ts` around lines 153 - 163, rebuildIndex can capture a non-null vaultKeypair and call searchIndexService.persistEncrypted even after logout; fix by checking the cancellation flag before and immediately after the async call: inside rebuildIndex read cancelledRef.current (or capture it) and return if true before calling persistEncrypted, and in the fire-and-forget promise chain check cancelledRef.current again before handling success/failure (or skip calling persistEncrypted entirely if cancelled), so that persistEncrypted is never invoked when clearIndex/logout has set cancelledRef. Reference functions/vars: rebuildIndex, persistEncrypted, cancelledRef, clearIndex, vaultKeypair, and ensure the check is performed inside the useCallback closure to avoid the captured stale vaultKeypair window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/hooks/useSearch.ts`:
- Line 96: persistEncrypted can race with clearIndex because it's called
fire-and-forget and its async writes may execute after cancelledRef.current is
set true by clearIndex; update persistEncrypted to accept an AbortSignal (or
take cancelledRef) and check signal.aborted (or cancelledRef.current)
immediately before performing the final IndexedDB write so it aborts the write
when cancelled, then change callers (the places that call persistEncrypted from
useSearch: where cancelledRef is set and where rebuildIndex triggers
persistEncrypted) to pass an AbortSignal (or re-check cancelledRef after
awaiting any asynchronous steps) so rebuildIndex and the fire-and-forget
persistEncrypted won't write stale encrypted data after
searchIndexService.clear() runs; ensure clearIndex creates/uses the same
AbortController to signal cancellation to any in-flight persistEncrypted calls.
---
Outside diff comments:
In `@apps/web/src/hooks/useSearch.ts`:
- Around line 153-163: rebuildIndex can capture a non-null vaultKeypair and call
searchIndexService.persistEncrypted even after logout; fix by checking the
cancellation flag before and immediately after the async call: inside
rebuildIndex read cancelledRef.current (or capture it) and return if true before
calling persistEncrypted, and in the fire-and-forget promise chain check
cancelledRef.current again before handling success/failure (or skip calling
persistEncrypted entirely if cancelled), so that persistEncrypted is never
invoked when clearIndex/logout has set cancelledRef. Reference functions/vars:
rebuildIndex, persistEncrypted, cancelledRef, clearIndex, vaultKeypair, and
ensure the check is performed inside the useCallback closure to avoid the
captured stale vaultKeypair window.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
.learnings/2026-02-24-run-e2e-locally-before-push.md.planning/STATE.md.planning/todos/pending/2026-02-24-async-incremental-search-index.mdapps/web/src/hooks/useSearch.ts
Summary
clearIndex()(on logout) fires whileinit()is suspended atawait loadEncrypted(), causing stale state writes and post-logout key material persistence to IndexedDBcancelledRefthatclearIndexsets to abort in-flight init after eachawaitpointAddresses: #198 (comment)
Test plan
isIndexReady=true🤖 Generated with Claude Code
Summary by CodeRabbit
Documentation
Chores