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
@@ -16,6 +16,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
import { GuardService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
@@ -56,12 +57,14 @@ export const NavigationPanelDocNode = ({
const t = useI18n();
const {
docsSearchService,
workspaceService,
docsService,
globalContextService,
docDisplayMetaService,
featureFlagService,
guardService,
} = useServices({
WorkspaceService,
DocsSearchService,
DocsService,
GlobalContextService,
@@ -120,9 +123,17 @@ export const NavigationPanelDocNode = ({
const [referencesLoading, setReferencesLoading] = useState(true);
useLayoutEffect(() => {
if (collapsed) {
return;
}
const abortController = new AbortController();
const undoSync = workspaceService.workspace.engine.doc.addPriority(
docId,
10
);
const undoIndexer = docsSearchService.indexer.addPriority(docId, 10);
docsSearchService.indexer
.waitForDocCompletedWithPriority(docId, 100, abortController.signal)
.waitForDocCompleted(docId, abortController.signal)
.then(() => {
setReferencesLoading(false);
})
@@ -132,9 +143,11 @@ export const NavigationPanelDocNode = ({
}
});
return () => {
undoSync();
undoIndexer();
abortController.abort(MANUALLY_STOP);
};
}, [docId, docsSearchService]);
}, [docId, docsSearchService, workspaceService, collapsed]);
const dndData = useMemo(() => {
return {
@@ -14,14 +14,14 @@ import type {
DatabaseRow,
DatabaseValueCell,
} from '@affine/core/modules/doc-info/types';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import { DocLinksService } from '@affine/core/modules/doc-link';
import { GuardService } from '@affine/core/modules/permissions';
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc';
import { LiveData, useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useMemo, useState } from 'react';
import { useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import * as styles from './info-modal.css';
import { LinksRow } from './links-row';
@@ -34,11 +34,11 @@ export const InfoTable = ({
onClose: () => void;
}) => {
const t = useI18n();
const { docsSearchService, workspacePropertyService, guardService } =
const { workspacePropertyService, guardService, docLinksService } =
useServices({
DocsSearchService,
WorkspacePropertyService,
GuardService,
DocLinksService,
});
const canEditPropertyInfo = useLiveData(
guardService.can$('Workspace_Properties_Update')
@@ -46,30 +46,9 @@ export const InfoTable = ({
const canEditProperty = useLiveData(guardService.can$('Doc_Update', docId));
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
const properties = useLiveData(workspacePropertyService.sortedProperties$);
const links = useLiveData(
useMemo(
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
[docId, docsSearchService]
)
);
const links = useLiveData(docLinksService.links.links$);
const backlinks = useLiveData(
useMemo(() => {
return LiveData.from(docsSearchService.watchRefsTo(docId), []).map(
links => {
const visitedDoc = new Set<string>();
// for each doc, we only show the first block
return links.filter(link => {
if (visitedDoc.has(link.docId)) {
return false;
}
visitedDoc.add(link.docId);
return true;
});
}
);
}, [docId, docsSearchService])
);
const backlinks = useLiveData(docLinksService.backlinks.backlinks$);
const onBacklinkPropertyChange = useCallback(
(_row: DatabaseRow, cell: DatabaseValueCell, _value: unknown) => {
@@ -111,6 +90,10 @@ export const InfoTable = ({
[]
);
useEffect(() => {
docLinksService.backlinks.revalidateFromCloud();
}, [docLinksService.backlinks]);
return (
<>
<PropertyCollapsibleSection