Skip to content

fix: adjust index loading timing#14470

Open
kthreadd wants to merge 4 commits intotoeverything:canaryfrom
kthreadd:bugfix/14191-unused-blob
Open

fix: adjust index loading timing#14470
kthreadd wants to merge 4 commits intotoeverything:canaryfrom
kthreadd:bugfix/14191-unused-blob

Conversation

@kthreadd
Copy link
Contributor

@kthreadd kthreadd commented Feb 18, 2026

fix #14467
fix #14191

In Electron apps, the default timer for flushing the FTS index (fts_flush_index) is 30 seconds. This can lead to a scenario where:

  1. A user modifies a document, triggering an indexer update.
  2. The updated indexes remain in memory because the flush timer hasn't expired.
  3. The user opens settings, causing a database reconnection.
  4. The in-memory indexes are lost due to idx_snapshot being reloaded.

This fix calls fts_flush_index before reconnecting the database when it is running, ensuring any pending index updates are flushed to disk and preventing data loss.

Summary by CodeRabbit

  • New Features

    • Added support for loading index snapshots before the indexing/sync loop and a native API to trigger FTS index snapshot loading.
    • Indexer can now delegate refresh responsibilities to storage implementations.
  • Bug Fixes

    • Removed redundant in-memory index initialization during connection startup.
  • Refactor

    • Workspace header titles now display live data directly (removed prior translation wrapping).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Removed i18n wrapping from desktop document titles; added a new loadIndexSnapshots step and delegated refresh responsibility to indexer storage in the indexing loop; introduced a native ftsLoadIndexSnapshots API and handlers; removed immediate in-memory index init after migrations in native connect.

Changes

Cohort / File(s) Summary
Frontend Title Display
packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx
Removed i18n translation wrapping from titles in JournalPageHeader and NormalPageHeader; title now uses live data directly (useLiveData(docDisplayMetaService.title$(page.id))).
Native connect change
packages/frontend/native/nbstore/src/storage.rs
Removed post-migrate self.init_index().await? call from connect, eliminating immediate in-memory index initialization after migrations.
Indexer interface & base
packages/common/nbstore/src/storage/indexer.ts
Added loadIndexSnapshots(): Promise<void> to IndexerStorage interface and IndexerStorageBase, expanding required storage API.
Indexer implementations
packages/common/nbstore/src/storage/dummy/indexer.ts, packages/common/nbstore/src/impls/cloud/indexer.ts, packages/common/nbstore/src/impls/idb/indexer/index.ts, packages/common/nbstore/src/impls/sqlite/indexer/index.ts
Added loadIndexSnapshots() overrides (no-op/TODO) and a Sqlite implementation delegating to native DB API.
Sqlite DB API & native binding
packages/common/nbstore/src/impls/sqlite/db.ts, packages/frontend/native/nbstore/src/lib.rs, packages/frontend/native/index.d.ts
Added native DB API ftsLoadIndexSnapshots(id: string): Promise<void> and Rust implementation fts_load_index_snapshots that calls init_index() for a storage id; updated TypeScript declaration.
Electron handlers
packages/frontend/apps/electron/src/helper/nbstore/handlers.ts
Registered ftsLoadIndexSnapshots handler forwarding to POOL.ftsLoadIndexSnapshots.bind(POOL).
Indexer sync loop
packages/common/nbstore/src/sync/indexer/index.ts
Call indexer.loadIndexSnapshots() before processing jobs; in finally block call indexer.refreshIfNeed() (delegate refresh to storage) instead of local refreshIfNeed().

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Sync as IndexerSyncImpl
participant Storage as IndexerStorage
participant DB as NativeDB/FTS
Sync->>Storage: loadIndexSnapshots()
Storage->>DB: ftsLoadIndexSnapshots(id)
DB-->>Storage: OK
Storage-->>Sync: snapshots loaded
Sync->>Sync: process indexing jobs
alt finally
Sync->>Storage: refreshIfNeed()
Storage->>DB: (maybe) init_index / refresh
DB-->>Storage: OK
Storage-->>Sync: refreshed
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudge the index, wake sleepy bytes,
titles hop out clearly, no translated plights.
Snapshots whispered, handlers in tune,
I twitch my nose — the search will soon bloom. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Translation wrapper removal in detail-page-header.tsx appears unrelated to FTS index persistence and database reconnection objectives. Remove the translation wrapper removal from detail-page-header.tsx or explain its necessity in relation to the primary index persistence objective.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive Changes address #14191 by preventing FTS index data loss through flush-before-reconnect; #14467 fix is only a translation wrapper removal for titles, unrelated to main PR objectives. Clarify whether the translation wrapper removal in detail-page-header.tsx is intentional and necessary, as it appears disconnected from the primary index persistence objective.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: adjust index loading timing' directly addresses the main objective of this PR: reordering the loadIndexSnapshots() call to occur after database connection to ensure pending FTS index updates are not lost during reconnection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kthreadd kthreadd force-pushed the bugfix/14191-unused-blob branch from 958268f to a28b3cd Compare February 18, 2026 16:09
@kthreadd kthreadd marked this pull request as ready for review February 18, 2026 16:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/frontend/native/nbstore/src/storage.rs`:
- Around line 66-69: The impl for SqliteDocStorage is missing the async methods
flush_index(&self) -> Result<()> and init_index(&self) -> Result<()>, causing
unresolved method errors where self.flush_index() and self.init_index() are
called; add these two pub async fn methods to the SqliteDocStorage impl that
delegate to the existing indexer logic (or inline the flush/init logic),
ensuring they acquire the same index read/write locks used elsewhere (e.g.,
where index.read().await.has_unpersisted_changes(None) is checked) and return
Result<()> so callers (including connect()) compile correctly; alternatively
move the flush/init calls to a place that has access to the indexer, but keep
the method signatures exactly as pub async fn flush_index(&self) -> Result<()>
and pub async fn init_index(&self) -> Result<()> and reference SqliteDocStorage
in the impl.

@kthreadd kthreadd marked this pull request as draft February 19, 2026 01:29
@kthreadd kthreadd force-pushed the bugfix/14191-unused-blob branch from a28b3cd to 0366105 Compare February 19, 2026 01:36
@kthreadd kthreadd changed the title fix: persist the indexes in memory before reconnecting the database fix: persist the in-memory indexes before reconnecting the database Feb 19, 2026
@kthreadd kthreadd marked this pull request as ready for review February 19, 2026 01:36
@darkskygit darkskygit enabled auto-merge February 19, 2026 02:21
@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.93%. Comparing base (35e1411) to head (7711c81).
⚠️ Report is 5 commits behind head on canary.

Files with missing lines Patch % Lines
packages/common/nbstore/src/sync/indexer/index.ts 0.00% 2 Missing ⚠️
...ages/common/nbstore/src/impls/idb/indexer/index.ts 0.00% 1 Missing ⚠️
...ckages/common/nbstore/src/storage/dummy/indexer.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           canary   #14470      +/-   ##
==========================================
- Coverage   57.51%   56.93%   -0.59%     
==========================================
  Files        2859     2859              
  Lines      154468   154471       +3     
  Branches    23336    23199     -137     
==========================================
- Hits        88845    87949     -896     
- Misses      62720    63613     +893     
- Partials     2903     2909       +6     
Flag Coverage Δ
server-test 74.96% <ø> (-1.15%) ⬇️
unittest 33.92% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

auto-merge was automatically disabled February 19, 2026 05:04

Head branch was pushed to by a user without write access

@kthreadd kthreadd marked this pull request as draft February 19, 2026 05:46
@github-actions github-actions bot added the app:electron Related to electron app label Feb 19, 2026
@kthreadd kthreadd marked this pull request as ready for review February 19, 2026 12:02
@kthreadd kthreadd requested a review from darkskygit February 19, 2026 12:02
@kthreadd
Copy link
Contributor Author

Force flush index to disk on stop and adjust snapshots loading timing.

TODO:
On macOS, Electron app may crash unexpectedly when exits, causing the index not to be flushed to disk.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/common/nbstore/src/impls/idb/indexer/index.ts (1)

228-231: Unimplemented loadIndexSnapshots for IDB — track follow-up

This is a permanent no-op with a TODO. If sync/indexer/index.ts now calls loadIndexSnapshots() at startup or after reconnection, IDB will silently skip snapshot loading. Confirm whether IDB needs actual snapshot initialization (e.g., loading an in-memory index from stored data) or whether the no-op is intentionally correct for browser storage.

Happy to help implement the IDB snapshot loading or open a tracking issue if you'd like.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/nbstore/src/impls/idb/indexer/index.ts` around lines 228 -
231, The override of loadIndexSnapshots() in the IDB indexer is a no-op which
will silently skip snapshot initialization when sync/indexer/index.ts calls it;
implement loading persisted snapshots from IndexedDB inside the IDB indexer (the
override async loadIndexSnapshots method) so the in-memory index is
reconstructed after startup/reconnect: read the saved snapshot records from the
IDB store used by this indexer, validate/deserialize them, and apply them to the
in-memory index via the indexer’s existing apply/restore method(s) (or call the
same snapshot-applier used by other storage backends); if implementing now is
not possible, replace the TODO with a clear comment and create a tracked issue
(and log/warn when loadIndexSnapshots is skipped) so callers are aware of the
missing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/common/nbstore/src/sync/indexer/index.ts`:
- Around line 481-482: The call to this.indexer.refreshIfNeed() can throw and
currently sits before unsubscribe(), so if it throws the subscribeDocUpdate
listener remains attached and this.status is retained; fix by wrapping the await
this.indexer.refreshIfNeed() (which triggers native ftsFlushIndex) in its own
try/finally that always calls unsubscribe() in the finally block so cleanup
always runs, ensuring unsubscribe() is executed even if refreshIfNeed() throws.
- Around line 210-211: Move the call to this.indexer.loadIndexSnapshots() out of
the pre-loop area and into retryLoop() immediately after waitForConnected()
resolves so the native FTS snapshot load runs only when the connection is
established and runs on every retry; also protect the unsubscribe() call by
ensuring it runs even if this.indexer.refreshIfNeed() throws (wrap
refreshIfNeed() in a try/catch or call unsubscribe() from a finally block) so
the subscribeDocUpdate listener is always removed; references:
loadIndexSnapshots, retryLoop, waitForConnected, refreshIfNeed, unsubscribe,
subscribeDocUpdate.

---

Nitpick comments:
In `@packages/common/nbstore/src/impls/idb/indexer/index.ts`:
- Around line 228-231: The override of loadIndexSnapshots() in the IDB indexer
is a no-op which will silently skip snapshot initialization when
sync/indexer/index.ts calls it; implement loading persisted snapshots from
IndexedDB inside the IDB indexer (the override async loadIndexSnapshots method)
so the in-memory index is reconstructed after startup/reconnect: read the saved
snapshot records from the IDB store used by this indexer, validate/deserialize
them, and apply them to the in-memory index via the indexer’s existing
apply/restore method(s) (or call the same snapshot-applier used by other storage
backends); if implementing now is not possible, replace the TODO with a clear
comment and create a tracked issue (and log/warn when loadIndexSnapshots is
skipped) so callers are aware of the missing behavior.

Comment on lines 210 to 211
await this.indexer.loadIndexSnapshots();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find NativeDBConnection implementation
echo "=== Locating NativeDBConnection ==="
fd -t f "\.ts$" | xargs grep -l "class NativeDBConnection" 2>/dev/null | head -5

# Also search for NativeDBConnection in the codebase
rg "class NativeDBConnection" -t ts -B 2 -A 10

Repository: toeverything/AFFiNE

Length of output: 1080


🏁 Script executed:

#!/bin/bash
# Check loadIndexSnapshots, ftsLoadIndexSnapshots placement and waitForConnected usage
echo "=== loadIndexSnapshots and waitForConnected in index.ts ==="
rg -n "loadIndexSnapshots|ftsLoadIndexSnapshots|waitForConnected" packages/common/nbstore/src/sync/indexer/index.ts -B 2 -A 2

echo "=== Check refreshIfNeed in finally block ==="
rg -n "refreshIfNeed|finally" packages/common/nbstore/src/sync/indexer/index.ts -B 3 -A 3

Repository: toeverything/AFFiNE

Length of output: 3783


🏁 Script executed:

#!/bin/bash
# Get context around mainLoop and retryLoop to see exact placement
echo "=== mainLoop function context (lines 205-225) ==="
sed -n '205,225p' packages/common/nbstore/src/sync/indexer/index.ts

echo "=== retryLoop function (find waitForConnected) ==="
rg -n "retryLoop\|waitForConnected" packages/common/nbstore/src/sync/indexer/index.ts -A 50 | head -80

Repository: toeverything/AFFiNE

Length of output: 756


🏁 Script executed:

#!/bin/bash
# Get full retryLoop function to see the structure
echo "=== retryLoop function (lines 235-485) ==="
sed -n '235,485p' packages/common/nbstore/src/sync/indexer/index.ts | head -100

echo ""
echo "=== Full finally block and unsubscribe ==="
sed -n '475,485p' packages/common/nbstore/src/sync/indexer/index.ts

Repository: toeverything/AFFiNE

Length of output: 3427


🏁 Script executed:

#!/bin/bash
# Check SqliteIndexerStorage.loadIndexSnapshots implementation
echo "=== SqliteIndexerStorage.loadIndexSnapshots ==="
rg -n "loadIndexSnapshots" -B 2 -A 10 --type ts | grep -A 12 "SqliteIndexerStorage\|class.*IndexerStorage"

echo ""
echo "=== ftsLoadIndexSnapshots definition ==="
rg -n "ftsLoadIndexSnapshots" -B 2 -A 5 --type ts

Repository: toeverything/AFFiNE

Length of output: 2601


🏁 Script executed:

#!/bin/bash
# Check if there's any connection guard inside loadIndexSnapshots
echo "=== Checking loadIndexSnapshots implementations across indexer storage classes ==="
fd -t f "\.ts$" | xargs grep -l "loadIndexSnapshots" | head -10

echo ""
echo "=== Full loadIndexSnapshots in each storage class ==="
for file in $(fd -t f "\.ts$" | xargs grep -l "loadIndexSnapshots" | head -5); do
  echo "File: $file"
  rg -n "loadIndexSnapshots" -B 2 -A 8 "$file"
  echo ""
done

Repository: toeverything/AFFiNE

Length of output: 1749


Move loadIndexSnapshots() into retryLoop() after waitForConnected() resolves, and protect unsubscribe() from refreshIfNeed() exceptions.

Three critical placement issues:

  1. Pre-connection call. loadIndexSnapshots() at line 210 executes before the while (true) loop begins, well before retryLoop() and its waitForConnected() calls. For SqliteIndexerStorage, this invokes the native API ftsLoadIndexSnapshots() against a potentially unestablished connection. Every other native API call in the codebase (lines 577, 580, 607, etc.) is guarded by waitForConnected() first. If this call fails, mainLoop() throws, the catch handler logs the error, and the indexer stops permanently.

  2. Not repeated on retry. loadIndexSnapshots() runs only once before the loop. On each retryLoop() failure, the connection may reset and the native FTS in-memory state may reinitialize. Subsequent retries skip the snapshot reload and operate on a potentially empty index.

  3. Unsubscribe unprotected in finally block. Lines 481–482 call await this.indexer.refreshIfNeed() before unsubscribe(). If refreshIfNeed() throws, unsubscribe() is never called, leaving the subscribeDocUpdate listener (established at line 270) dangling indefinitely.

🔧 Suggested fixes
// Move loadIndexSnapshots into retryLoop after waitForConnected
  private async retryLoop(signal?: AbortSignal) {
    await Promise.race([...]);

    this.status.errorMessage = null;
    this.status.statusUpdatedSubject$.next(true);

+   await this.indexer.loadIndexSnapshots();
+
    const indexVersion = await this.indexer.indexVersion();

// Protect unsubscribe in finally block
  } finally {
+   try {
      await this.indexer.refreshIfNeed();
+   } finally {
      unsubscribe();
+   }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/nbstore/src/sync/indexer/index.ts` around lines 210 - 211,
Move the call to this.indexer.loadIndexSnapshots() out of the pre-loop area and
into retryLoop() immediately after waitForConnected() resolves so the native FTS
snapshot load runs only when the connection is established and runs on every
retry; also protect the unsubscribe() call by ensuring it runs even if
this.indexer.refreshIfNeed() throws (wrap refreshIfNeed() in a try/catch or call
unsubscribe() from a finally block) so the subscribeDocUpdate listener is always
removed; references: loadIndexSnapshots, retryLoop, waitForConnected,
refreshIfNeed, unsubscribe, subscribeDocUpdate.

Comment on lines +481 to 482
await this.indexer.refreshIfNeed();
unsubscribe();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

unsubscribe() is skipped if this.indexer.refreshIfNeed() throws.

Before this PR the finally block only contained unsubscribe(), which is synchronous and cannot throw. The new await this.indexer.refreshIfNeed() (which calls ftsFlushIndex on the native side) can throw — e.g., if the connection was reset. If it does, unsubscribe() is never reached, leaving the subscribeDocUpdate listener (line 270) permanently attached and holding references to this.status, causing a memory/behavioral leak.

Wrap the flush in its own try/finally so cleanup is guaranteed:

🛡️ Proposed fix
     } finally {
-      await this.indexer.refreshIfNeed();
-      unsubscribe();
+      try {
+        await this.indexer.refreshIfNeed();
+      } finally {
+        unsubscribe();
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.indexer.refreshIfNeed();
unsubscribe();
try {
await this.indexer.refreshIfNeed();
} finally {
unsubscribe();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/nbstore/src/sync/indexer/index.ts` around lines 481 - 482,
The call to this.indexer.refreshIfNeed() can throw and currently sits before
unsubscribe(), so if it throws the subscribeDocUpdate listener remains attached
and this.status is retained; fix by wrapping the await
this.indexer.refreshIfNeed() (which triggers native ftsFlushIndex) in its own
try/finally that always calls unsubscribe() in the finally block so cleanup
always runs, ensuring unsubscribe() is executed even if refreshIfNeed() throws.

@kthreadd kthreadd marked this pull request as draft February 20, 2026 06:24
@kthreadd kthreadd marked this pull request as ready for review February 20, 2026 13:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/common/nbstore/src/sync/indexer/index.ts`:
- Around line 480-483: The finally block currently awaits
this.indexer.refreshIfNeed() and then calls unsubscribe(), but if
refreshIfNeed() throws the unsubscribe() call is skipped leaving the
subscribeDocUpdate listener attached; change the finally so unsubscribe() always
runs regardless of refreshIfNeed() outcome — e.g. wrap the refreshIfNeed() call
in its own try/catch (or perform refresh in a try and put unsubscribe() in a
nested finally) so that unsubscribe() is invoked unconditionally; reference
this.indexer.refreshIfNeed(), unsubscribe(), and the subscribeDocUpdate listener
to locate and update the code.

@kthreadd kthreadd changed the title fix: persist the in-memory indexes before reconnecting the database fix: adjust index loading timing Feb 21, 2026
@kthreadd
Copy link
Contributor Author

@darkskygit Hello, could this pr merge into master?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Bug]: unused blobs not accurate

2 participants