mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
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:
@@ -1,13 +1,16 @@
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
first,
|
||||
lastValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
share,
|
||||
Subject,
|
||||
throttleTime,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
applyUpdate,
|
||||
@@ -22,6 +25,7 @@ import type { DocRecord, DocStorage } from '../storage';
|
||||
import type { DocSync } from '../sync/doc';
|
||||
import { AsyncPriorityQueue } from '../utils/async-priority-queue';
|
||||
import { isEmptyUpdate } from '../utils/is-empty-update';
|
||||
import { takeUntilAbort } from '../utils/take-until-abort';
|
||||
import { MANUALLY_STOP, throwIfAborted } from '../utils/throw-if-aborted';
|
||||
|
||||
const NBSTORE_ORIGIN = 'nbstore-frontend';
|
||||
@@ -86,6 +90,10 @@ export type DocFrontendState = {
|
||||
* number of docs that have been loaded to yjs doc instance
|
||||
*/
|
||||
loaded: number;
|
||||
/**
|
||||
* some data is being applied to yjs doc instance, or some data is being saved to local doc storage
|
||||
*/
|
||||
updating: boolean;
|
||||
/**
|
||||
* number of docs that are syncing with remote peers
|
||||
*/
|
||||
@@ -128,7 +136,7 @@ export class DocFrontend {
|
||||
readonly options: DocFrontendOptions = {}
|
||||
) {}
|
||||
|
||||
docState$(docId: string): Observable<DocFrontendDocState> {
|
||||
private _docState$(docId: string): Observable<DocFrontendDocState> {
|
||||
const frontendState$ = new Observable<{
|
||||
ready: boolean;
|
||||
loaded: boolean;
|
||||
@@ -160,24 +168,38 @@ export class DocFrontend {
|
||||
);
|
||||
}
|
||||
|
||||
state$ = combineLatest([
|
||||
new Observable<{ total: number; loaded: number }>(subscriber => {
|
||||
const next = () => {
|
||||
subscriber.next({
|
||||
total: this.status.docs.size,
|
||||
loaded: this.status.connectedDocs.size,
|
||||
});
|
||||
};
|
||||
next();
|
||||
return this.statusUpdatedSubject$.subscribe(() => {
|
||||
docState$(docId: string): Observable<DocFrontendDocState> {
|
||||
return this._docState$(docId).pipe(
|
||||
throttleTime(1000, undefined, {
|
||||
trailing: true,
|
||||
leading: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private readonly _state$ = combineLatest([
|
||||
new Observable<{ total: number; loaded: number; updating: boolean }>(
|
||||
subscriber => {
|
||||
const next = () => {
|
||||
subscriber.next({
|
||||
total: this.status.docs.size,
|
||||
loaded: this.status.connectedDocs.size,
|
||||
updating:
|
||||
this.status.jobMap.size > 0 || this.status.currentJob !== null,
|
||||
});
|
||||
};
|
||||
next();
|
||||
});
|
||||
}),
|
||||
return this.statusUpdatedSubject$.subscribe(() => {
|
||||
next();
|
||||
});
|
||||
}
|
||||
),
|
||||
this.sync.state$,
|
||||
]).pipe(
|
||||
map(([frontend, sync]) => ({
|
||||
total: sync.total ?? frontend.total,
|
||||
loaded: frontend.loaded,
|
||||
updating: frontend.updating,
|
||||
syncing: sync.syncing,
|
||||
synced: sync.synced,
|
||||
syncRetrying: sync.retrying,
|
||||
@@ -188,6 +210,13 @@ export class DocFrontend {
|
||||
})
|
||||
) satisfies Observable<DocFrontendState>;
|
||||
|
||||
state$ = this._state$.pipe(
|
||||
throttleTime(1000, undefined, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
})
|
||||
);
|
||||
|
||||
start() {
|
||||
if (this.abort.signal.aborted) {
|
||||
throw new Error('doc frontend can only start once');
|
||||
@@ -463,96 +492,43 @@ ${changedList}
|
||||
return merge(updates.filter(bin => !isEmptyUpdate(bin)));
|
||||
}
|
||||
|
||||
async waitForSynced(abort?: AbortSignal) {
|
||||
let sub: Subscription | undefined = undefined;
|
||||
return Promise.race([
|
||||
new Promise<void>(resolve => {
|
||||
sub = this.state$?.subscribe(status => {
|
||||
if (status.synced) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
new Promise<void>((_, reject) => {
|
||||
if (abort?.aborted) {
|
||||
reject(abort?.reason);
|
||||
}
|
||||
abort?.addEventListener('abort', () => {
|
||||
reject(abort.reason);
|
||||
});
|
||||
}),
|
||||
]).finally(() => {
|
||||
sub?.unsubscribe();
|
||||
});
|
||||
async waitForUpdated(docId?: string, abort?: AbortSignal) {
|
||||
const source$: Observable<DocFrontendDocState | DocFrontendState> = docId
|
||||
? this._docState$(docId)
|
||||
: this._state$;
|
||||
await lastValueFrom(
|
||||
source$.pipe(
|
||||
filter(status => !status.updating),
|
||||
takeUntilAbort(abort),
|
||||
first()
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
async waitForDocLoaded(docId: string, abort?: AbortSignal) {
|
||||
let sub: Subscription | undefined = undefined;
|
||||
return Promise.race([
|
||||
new Promise<void>(resolve => {
|
||||
sub = this.docState$(docId).subscribe(state => {
|
||||
if (state.loaded) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
new Promise<void>((_, reject) => {
|
||||
if (abort?.aborted) {
|
||||
reject(abort?.reason);
|
||||
}
|
||||
abort?.addEventListener('abort', () => {
|
||||
reject(abort.reason);
|
||||
});
|
||||
}),
|
||||
]).finally(() => {
|
||||
sub?.unsubscribe();
|
||||
});
|
||||
await lastValueFrom(
|
||||
this._docState$(docId).pipe(
|
||||
filter(state => state.loaded),
|
||||
takeUntilAbort(abort),
|
||||
first()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async waitForDocSynced(docId: string, abort?: AbortSignal) {
|
||||
let sub: Subscription | undefined = undefined;
|
||||
return Promise.race([
|
||||
new Promise<void>(resolve => {
|
||||
sub = this.docState$(docId).subscribe(state => {
|
||||
if (state.synced && !state.updating) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
new Promise<void>((_, reject) => {
|
||||
if (abort?.aborted) {
|
||||
reject(abort?.reason);
|
||||
}
|
||||
abort?.addEventListener('abort', () => {
|
||||
reject(abort.reason);
|
||||
});
|
||||
}),
|
||||
]).finally(() => {
|
||||
sub?.unsubscribe();
|
||||
});
|
||||
async waitForSynced(docId?: string, abort?: AbortSignal) {
|
||||
await this.waitForUpdated(docId, abort);
|
||||
await this.sync.waitForSynced(docId, abort);
|
||||
}
|
||||
|
||||
async waitForDocReady(docId: string, abort?: AbortSignal) {
|
||||
let sub: Subscription | undefined = undefined;
|
||||
return Promise.race([
|
||||
new Promise<void>(resolve => {
|
||||
sub = this.docState$(docId).subscribe(state => {
|
||||
if (state.ready) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
new Promise<void>((_, reject) => {
|
||||
if (abort?.aborted) {
|
||||
reject(abort?.reason);
|
||||
}
|
||||
abort?.addEventListener('abort', () => {
|
||||
reject(abort.reason);
|
||||
});
|
||||
}),
|
||||
]).finally(() => {
|
||||
sub?.unsubscribe();
|
||||
});
|
||||
await lastValueFrom(
|
||||
this._docState$(docId).pipe(
|
||||
filter(state => state.ready),
|
||||
takeUntilAbort(abort),
|
||||
first()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async resetSync() {
|
||||
|
||||
Reference in New Issue
Block a user