refactor(nbstore): improve doc state management (#12359)

Move the `waitForSynced` method from `frontend` to `nbstore worker` to make the wait more reliable

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added explicit tracking of document updating state to indicate when data is being applied or saved.
  - Introduced new methods to wait for update and synchronization completion with abort support.

- **Improvements**
  - Applied throttling with leading and trailing emissions to state observables for smoother UI updates.
  - Refined synchronization waiting logic for clearer separation between update completion and sync completion.
  - Removed throttling in workspace selector component for more immediate state feedback.
  - Updated import and clipper services to use the new synchronization waiting methods.
  - Simplified asynchronous waiting logic in indexer synchronization methods.

- **Bug Fixes**
  - Enhanced accuracy and reliability of document update and sync status indicators.

- **Tests**
  - Increased wait timeout in avatar selection test to improve stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-05-20 07:33:20 +00:00
parent 151f499154
commit 59ef4b227b
10 changed files with 157 additions and 167 deletions
@@ -2,6 +2,7 @@ import { readAllDocsFromRootDoc } from '@affine/reader';
import {
filter,
first,
lastValueFrom,
Observable,
ReplaySubject,
share,
@@ -71,60 +72,37 @@ export class IndexerSyncImpl implements IndexerSync {
state$ = this.status.state$.pipe(
// throttle the state to 1 second to avoid spamming the UI
throttleTime(1000)
throttleTime(1000, undefined, {
leading: true,
trailing: true,
})
);
docState$(docId: string) {
return this.status.docState$(docId).pipe(
// throttle the state to 1 second to avoid spamming the UI
throttleTime(1000)
throttleTime(1000, undefined, { leading: true, trailing: true })
);
}
waitForCompleted(signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
this.status.state$
.pipe(
filter(state => state.completed),
takeUntilAbort(signal),
first()
)
.subscribe({
next: () => {
resolve();
},
error: err => {
reject(err);
},
});
});
}
waitForDocCompleted(docId: string, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
this.status
.docState$(docId)
.pipe(
filter(state => state.completed),
takeUntilAbort(signal),
first()
)
.subscribe({
next: () => {
resolve();
},
error: err => {
reject(err);
},
});
});
}
readonly interval = () =>
new Promise<void>(resolve =>
requestIdleCallback(() => resolve(), {
timeout: 200,
})
async waitForCompleted(signal?: AbortSignal) {
await lastValueFrom(
this.status.state$.pipe(
filter(state => state.completed),
takeUntilAbort(signal),
first()
)
);
}
async waitForDocCompleted(docId: string, signal?: AbortSignal) {
await lastValueFrom(
this.status.docState$(docId).pipe(
filter(state => state.completed),
takeUntilAbort(signal),
first()
)
);
}
constructor(
readonly doc: DocStorage,