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:
EYHN
2025-08-01 16:31:31 +08:00
committed by GitHub
parent 1661ab1790
commit 1ceed6c145
34 changed files with 717 additions and 286 deletions

View File

@@ -1,7 +1,20 @@
import { Entity, LiveData } from '@toeverything/infra';
import {
catchErrorInto,
effect,
Entity,
exhaustMapWithTrailing,
fromPromise,
LiveData,
onComplete,
onStart,
smartRetry,
} from '@toeverything/infra';
import { tap } from 'rxjs';
import type { DocService } from '../../doc';
import type { DocService, DocsService } from '../../doc';
import type { DocsSearchService } from '../../docs-search';
import type { FeatureFlagService } from '../../feature-flag';
import type { WorkspaceService } from '../../workspace';
export interface Backlink {
docId: string;
@@ -17,13 +30,128 @@ export interface Backlink {
export class DocBacklinks extends Entity {
constructor(
private readonly docsSearchService: DocsSearchService,
private readonly docService: DocService
private readonly docService: DocService,
private readonly docsService: DocsService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceService: WorkspaceService
) {
super();
}
backlinks$ = LiveData.from<Backlink[]>(
this.docsSearchService.watchRefsTo(this.docService.doc.id),
[]
backlinks$ = new LiveData<Backlink[] | undefined>(undefined);
isLoading$ = new LiveData<boolean>(false);
error$ = new LiveData<any>(undefined);
revalidateFromCloud = effect(
exhaustMapWithTrailing(() =>
fromPromise(async () => {
const searchFromCloud =
this.featureFlagService.flags.enable_battery_save_mode &&
this.workspaceService.workspace.flavour !== 'local';
const { buckets } = await this.docsSearchService.indexer.aggregate(
'block',
{
type: 'boolean',
occur: 'must',
queries: [
{
type: 'match',
field: 'refDocId',
match: this.docService.doc.id,
},
],
},
'docId',
{
hits: {
fields: [
'docId',
'blockId',
'parentBlockId',
'parentFlavour',
'additional',
'markdownPreview',
],
pagination: {
limit: 5, // the max number of backlinks to show for each doc
},
},
pagination: {
limit: 100,
},
prefer: searchFromCloud ? 'remote' : 'local',
}
);
return buckets.flatMap(bucket => {
const title =
this.docsService.list.doc$(bucket.key).value?.title$.value ?? '';
if (bucket.key === this.docService.doc.id) {
// Ignore if it is a link to the current document.
return [];
}
return bucket.hits.nodes.map(node => {
const blockId = node.fields.blockId ?? '';
const markdownPreview = node.fields.markdownPreview ?? '';
const additional =
typeof node.fields.additional === 'string'
? node.fields.additional
: node.fields.additional[0];
const additionalData: {
displayMode?: string;
noteBlockId?: string;
} = JSON.parse(additional || '{}');
const displayMode = additionalData.displayMode ?? '';
const noteBlockId = additionalData.noteBlockId ?? '';
const parentBlockId =
typeof node.fields.parentBlockId === 'string'
? node.fields.parentBlockId
: node.fields.parentBlockId[0];
const parentFlavour =
typeof node.fields.parentFlavour === 'string'
? node.fields.parentFlavour
: node.fields.parentFlavour[0];
return {
docId: bucket.key,
blockId: typeof blockId === 'string' ? blockId : blockId[0],
title: title,
markdownPreview:
typeof markdownPreview === 'string'
? markdownPreview
: markdownPreview[0],
displayMode:
typeof displayMode === 'string' ? displayMode : displayMode[0],
noteBlockId:
typeof noteBlockId === 'string' ? noteBlockId : noteBlockId[0],
parentBlockId:
typeof parentBlockId === 'string'
? parentBlockId
: parentBlockId[0],
parentFlavour:
typeof parentFlavour === 'string'
? parentFlavour
: parentFlavour[0],
};
});
});
}).pipe(
smartRetry(),
tap(backlinks => {
this.backlinks$.value = backlinks;
}),
catchErrorInto(this.error$),
onStart(() => {
this.isLoading$.value = true;
}),
onComplete(() => {
this.isLoading$.value = false;
})
)
)
);
}

View File

@@ -2,8 +2,10 @@ import { type Framework } from '@toeverything/infra';
import { DocScope } from '../doc/scopes/doc';
import { DocService } from '../doc/services/doc';
import { DocsService } from '../doc/services/docs';
import { DocsSearchService } from '../docs-search';
import { WorkspaceScope } from '../workspace';
import { FeatureFlagService } from '../feature-flag';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { DocBacklinks } from './entities/doc-backlinks';
import { DocLinks } from './entities/doc-links';
import { DocLinksService } from './services/doc-links';
@@ -17,6 +19,12 @@ export function configureDocLinksModule(framework: Framework) {
.scope(WorkspaceScope)
.scope(DocScope)
.service(DocLinksService)
.entity(DocBacklinks, [DocsSearchService, DocService])
.entity(DocBacklinks, [
DocsSearchService,
DocService,
DocsService,
FeatureFlagService,
WorkspaceService,
])
.entity(DocLinks, [DocsSearchService, DocService]);
}