mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
feat(core): support better battery save mode (#13383)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a Document Summary module, enabling live and cached document summaries with cloud revalidation. * Added a feature flag for enabling battery save mode. * Added explicit pause and resume controls for sync operations, accessible via UI events and programmatically. * **Improvements** * Enhanced sync and indexing logic to support pausing, resuming, and battery save mode, with improved job prioritization. * Updated navigation and preview components to use the new document summary service and improved priority handling. * Improved logging and state reporting for sync and indexing processes. * Refined backlink handling with reactive loading states and cloud revalidation. * Replaced backlink and link management to use a new dedicated document links service. * Enhanced workspace engine to conditionally enable battery save mode based on feature flags and workspace flavor. * **Bug Fixes** * Removed unnecessary debug console logs from various components for cleaner output. * **Refactor** * Replaced battery save mode methods with explicit pause/resume methods throughout the app and services. * Modularized and streamlined document summary and sync-related code for better maintainability. * Restructured backlink components to improve visibility handling and data fetching. * Simplified and improved document backlink data fetching with retry and loading state management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
7
packages/common/graphql/src/graphql/get-doc-summary.gql
Normal file
7
packages/common/graphql/src/graphql/get-doc-summary.gql
Normal file
@@ -0,0 +1,7 @@
|
||||
query getDocSummary($workspaceId: String!, $docId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
doc(docId: $docId) {
|
||||
summary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1454,6 +1454,18 @@ export const getDocDefaultRoleQuery = {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getDocSummaryQuery = {
|
||||
id: 'getDocSummaryQuery' as const,
|
||||
op: 'getDocSummary',
|
||||
query: `query getDocSummary($workspaceId: String!, $docId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
doc(docId: $docId) {
|
||||
summary
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getInviteInfoQuery = {
|
||||
id: 'getInviteInfoQuery' as const,
|
||||
op: 'getInviteInfo',
|
||||
|
||||
@@ -5077,6 +5077,19 @@ export type GetDocDefaultRoleQuery = {
|
||||
};
|
||||
};
|
||||
|
||||
export type GetDocSummaryQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
docId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type GetDocSummaryQuery = {
|
||||
__typename?: 'Query';
|
||||
workspace: {
|
||||
__typename?: 'WorkspaceType';
|
||||
doc: { __typename?: 'DocType'; summary: string | null };
|
||||
};
|
||||
};
|
||||
|
||||
export type GetInviteInfoQueryVariables = Exact<{
|
||||
inviteId: Scalars['String']['input'];
|
||||
}>;
|
||||
@@ -6432,6 +6445,11 @@ export type Queries =
|
||||
variables: GetDocDefaultRoleQueryVariables;
|
||||
response: GetDocDefaultRoleQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getDocSummaryQuery';
|
||||
variables: GetDocSummaryQueryVariables;
|
||||
response: GetDocSummaryQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getInviteInfoQuery';
|
||||
variables: GetInviteInfoQueryVariables;
|
||||
|
||||
@@ -249,7 +249,7 @@ export class DocFrontend {
|
||||
while (true) {
|
||||
throwIfAborted(signal);
|
||||
|
||||
const docId = await this.status.jobDocQueue.asyncPop(signal);
|
||||
const docId = await this.status.jobDocQueue.asyncPop(undefined, signal);
|
||||
const jobs = this.status.jobMap.get(docId);
|
||||
this.status.jobMap.delete(docId);
|
||||
|
||||
|
||||
@@ -712,7 +712,7 @@ export class DocSyncPeer {
|
||||
while (true) {
|
||||
throwIfAborted(signal);
|
||||
|
||||
const docId = await this.status.jobDocQueue.asyncPop(signal);
|
||||
const docId = await this.status.jobDocQueue.asyncPop(undefined, signal);
|
||||
|
||||
while (true) {
|
||||
// batch process jobs for the same doc
|
||||
|
||||
@@ -36,6 +36,8 @@ import { crawlingDocData } from './crawler';
|
||||
export type IndexerPreferOptions = 'local' | 'remote';
|
||||
|
||||
export interface IndexerSyncState {
|
||||
paused: boolean;
|
||||
batterySaveMode: boolean;
|
||||
/**
|
||||
* Number of documents currently in the indexing queue
|
||||
*/
|
||||
@@ -167,6 +169,14 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
this.status.disableBatterySaveMode();
|
||||
}
|
||||
|
||||
pauseSync() {
|
||||
this.status.pauseSync();
|
||||
}
|
||||
|
||||
resumeSync() {
|
||||
this.status.resumeSync();
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.abort) {
|
||||
this.abort.abort(MANUALLY_STOP);
|
||||
@@ -324,6 +334,7 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
const docId = await this.status.acceptJob(signal);
|
||||
|
||||
if (docId === this.rootDocId) {
|
||||
console.log('[indexer] start indexing root doc', docId);
|
||||
// #region crawl root doc
|
||||
for (const [docId, { title }] of this.status.docsInRootDoc) {
|
||||
const existingDoc = this.status.docsInIndexer.get(docId);
|
||||
@@ -401,6 +412,7 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
// doc is deleted, just skip
|
||||
continue;
|
||||
}
|
||||
console.log('[indexer] start indexing doc', docId);
|
||||
const docYDoc = new YDoc({ guid: docId });
|
||||
applyUpdate(docYDoc, docBin.bin);
|
||||
|
||||
@@ -454,6 +466,8 @@ export class IndexerSyncImpl implements IndexerSync {
|
||||
// #endregion
|
||||
}
|
||||
|
||||
console.log('[indexer] complete job', docId);
|
||||
|
||||
this.status.completeJob();
|
||||
}
|
||||
} finally {
|
||||
@@ -619,10 +633,11 @@ class IndexerSyncStatus {
|
||||
currentJob: string | null = null;
|
||||
errorMessage: string | null = null;
|
||||
statusUpdatedSubject$ = new Subject<string | true>();
|
||||
batterySaveMode: {
|
||||
paused: {
|
||||
promise: Promise<void>;
|
||||
resolve: () => void;
|
||||
} | null = null;
|
||||
batterySaveMode: boolean = false;
|
||||
|
||||
state$ = new Observable<IndexerSyncState>(subscribe => {
|
||||
const next = () => {
|
||||
@@ -632,6 +647,8 @@ class IndexerSyncStatus {
|
||||
total: 0,
|
||||
errorMessage: this.errorMessage,
|
||||
completed: true,
|
||||
batterySaveMode: this.batterySaveMode,
|
||||
paused: this.paused !== null,
|
||||
});
|
||||
} else {
|
||||
subscribe.next({
|
||||
@@ -639,6 +656,8 @@ class IndexerSyncStatus {
|
||||
total: this.docsInRootDoc.size + 1,
|
||||
errorMessage: this.errorMessage,
|
||||
completed: this.rootDocReady && this.jobs.length() === 0,
|
||||
batterySaveMode: this.batterySaveMode,
|
||||
paused: this.paused !== null,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -697,10 +716,14 @@ class IndexerSyncStatus {
|
||||
}
|
||||
|
||||
async acceptJob(abort?: AbortSignal) {
|
||||
if (this.batterySaveMode) {
|
||||
await this.batterySaveMode.promise;
|
||||
if (this.paused) {
|
||||
await this.paused.promise;
|
||||
}
|
||||
const job = await this.jobs.asyncPop(abort);
|
||||
const job = await this.jobs.asyncPop(
|
||||
// if battery save mode is enabled, only accept jobs with priority > 1; otherwise accept all jobs
|
||||
this.batterySaveMode ? 1 : undefined,
|
||||
abort
|
||||
);
|
||||
this.currentJob = job;
|
||||
this.statusUpdatedSubject$.next(job);
|
||||
return job;
|
||||
@@ -728,12 +751,33 @@ class IndexerSyncStatus {
|
||||
if (this.batterySaveMode) {
|
||||
return;
|
||||
}
|
||||
this.batterySaveMode = Promise.withResolvers();
|
||||
this.batterySaveMode = true;
|
||||
this.statusUpdatedSubject$.next(true);
|
||||
}
|
||||
|
||||
disableBatterySaveMode() {
|
||||
this.batterySaveMode?.resolve();
|
||||
this.batterySaveMode = null;
|
||||
if (!this.batterySaveMode) {
|
||||
return;
|
||||
}
|
||||
this.batterySaveMode = false;
|
||||
this.statusUpdatedSubject$.next(true);
|
||||
}
|
||||
|
||||
pauseSync() {
|
||||
if (this.paused) {
|
||||
return;
|
||||
}
|
||||
this.paused = Promise.withResolvers();
|
||||
this.statusUpdatedSubject$.next(true);
|
||||
}
|
||||
|
||||
resumeSync() {
|
||||
if (!this.paused) {
|
||||
return;
|
||||
}
|
||||
this.paused.resolve();
|
||||
this.paused = null;
|
||||
this.statusUpdatedSubject$.next(true);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -745,6 +789,8 @@ class IndexerSyncStatus {
|
||||
this.rootDoc = new YDoc();
|
||||
this.rootDocReady = false;
|
||||
this.currentJob = null;
|
||||
this.batterySaveMode = false;
|
||||
this.paused = null;
|
||||
this.statusUpdatedSubject$.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ export class AsyncPriorityQueue extends PriorityQueue {
|
||||
private _resolveUpdate: (() => void) | null = null;
|
||||
private _waitForUpdate: Promise<void> | null = null;
|
||||
|
||||
async asyncPop(abort?: AbortSignal): Promise<string> {
|
||||
const update = this.pop();
|
||||
async asyncPop(
|
||||
minimumPriority?: number,
|
||||
abort?: AbortSignal
|
||||
): Promise<string> {
|
||||
const update = this.pop(minimumPriority);
|
||||
if (update) {
|
||||
return update;
|
||||
} else {
|
||||
@@ -27,7 +30,7 @@ export class AsyncPriorityQueue extends PriorityQueue {
|
||||
}),
|
||||
]);
|
||||
|
||||
return this.asyncPop(abort);
|
||||
return this.asyncPop(minimumPriority, abort);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,20 @@ export class PriorityQueue {
|
||||
this.priorityMap.set(id, priority);
|
||||
}
|
||||
|
||||
pop() {
|
||||
pop(minimumPriority?: number) {
|
||||
const node = this.tree.max();
|
||||
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
minimumPriority !== undefined &&
|
||||
node.getValue().priority < minimumPriority
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.tree.removeNode(node);
|
||||
|
||||
const { id } = node.getValue();
|
||||
|
||||
@@ -87,17 +87,18 @@ export class StoreManagerClient {
|
||||
});
|
||||
}
|
||||
|
||||
enableBatterySaveMode() {
|
||||
pause() {
|
||||
this.connections.forEach(connection => {
|
||||
connection.store.enableBatterySaveMode().catch(err => {
|
||||
console.error('error enabling battery save mode', err);
|
||||
connection.store.pauseSync().catch(err => {
|
||||
console.error('error pausing', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
disableBatterySaveMode() {
|
||||
|
||||
resume() {
|
||||
this.connections.forEach(connection => {
|
||||
connection.store.disableBatterySaveMode().catch(err => {
|
||||
console.error('error disabling battery save mode', err);
|
||||
connection.store.resumeSync().catch(err => {
|
||||
console.error('error resuming', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -135,6 +136,13 @@ export class StoreClient {
|
||||
disableBatterySaveMode(): Promise<void> {
|
||||
return this.client.call('sync.disableBatterySaveMode');
|
||||
}
|
||||
|
||||
pauseSync() {
|
||||
return this.client.call('sync.pauseSync');
|
||||
}
|
||||
resumeSync() {
|
||||
return this.client.call('sync.resumeSync');
|
||||
}
|
||||
}
|
||||
|
||||
class WorkerDocStorage implements DocStorage {
|
||||
|
||||
@@ -135,34 +135,44 @@ class StoreConsumer {
|
||||
}
|
||||
|
||||
private readonly ENABLE_BATTERY_SAVE_MODE_DELAY = 1000;
|
||||
private enableBatterySaveModeTimeout: NodeJS.Timeout | null = null;
|
||||
private enabledBatterySaveMode = false;
|
||||
private syncPauseTimeout: NodeJS.Timeout | null = null;
|
||||
private syncPaused = false;
|
||||
|
||||
enableBatterySaveMode() {
|
||||
if (this.enableBatterySaveModeTimeout || this.enabledBatterySaveMode) {
|
||||
private pauseSync() {
|
||||
if (this.syncPauseTimeout || this.syncPaused) {
|
||||
return;
|
||||
}
|
||||
this.enableBatterySaveModeTimeout = setTimeout(() => {
|
||||
if (!this.enabledBatterySaveMode) {
|
||||
this.indexerSync.enableBatterySaveMode();
|
||||
this.enabledBatterySaveMode = true;
|
||||
console.log('[BatterySaveMode] enabled');
|
||||
this.syncPauseTimeout = setTimeout(() => {
|
||||
if (!this.syncPaused) {
|
||||
this.indexerSync.pauseSync();
|
||||
this.syncPaused = true;
|
||||
console.log('[IndexerSync] paused');
|
||||
}
|
||||
}, this.ENABLE_BATTERY_SAVE_MODE_DELAY);
|
||||
}
|
||||
|
||||
disableBatterySaveMode() {
|
||||
if (this.enableBatterySaveModeTimeout) {
|
||||
clearTimeout(this.enableBatterySaveModeTimeout);
|
||||
this.enableBatterySaveModeTimeout = null;
|
||||
private resumeSync() {
|
||||
if (this.syncPauseTimeout) {
|
||||
clearTimeout(this.syncPauseTimeout);
|
||||
this.syncPauseTimeout = null;
|
||||
}
|
||||
if (this.enabledBatterySaveMode) {
|
||||
this.indexerSync.disableBatterySaveMode();
|
||||
this.enabledBatterySaveMode = false;
|
||||
console.log('[BatterySaveMode] disabled');
|
||||
if (this.syncPaused) {
|
||||
this.indexerSync.resumeSync();
|
||||
this.syncPaused = false;
|
||||
console.log('[IndexerSync] resumed');
|
||||
}
|
||||
}
|
||||
|
||||
private enableBatterySaveMode() {
|
||||
console.log('[IndexerSync] enable battery save mode');
|
||||
this.indexerSync.enableBatterySaveMode();
|
||||
}
|
||||
|
||||
private disableBatterySaveMode() {
|
||||
console.log('[IndexerSync] disable battery save mode');
|
||||
this.indexerSync.disableBatterySaveMode();
|
||||
}
|
||||
|
||||
private registerHandlers(consumer: OpConsumer<WorkerOps>) {
|
||||
const collectJobs = new Map<
|
||||
string,
|
||||
@@ -316,6 +326,8 @@ class StoreConsumer {
|
||||
this.indexerSync.aggregate$(table, query, field, options),
|
||||
'sync.enableBatterySaveMode': () => this.enableBatterySaveMode(),
|
||||
'sync.disableBatterySaveMode': () => this.disableBatterySaveMode(),
|
||||
'sync.pauseSync': () => this.pauseSync(),
|
||||
'sync.resumeSync': () => this.resumeSync(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,8 @@ interface GroupedWorkerOps {
|
||||
sync: {
|
||||
enableBatterySaveMode: [void, void];
|
||||
disableBatterySaveMode: [void, void];
|
||||
pauseSync: [void, void];
|
||||
resumeSync: [void, void];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user