From b2bf5914631635a1277ece0940d1c0024293ac46 Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 3 Jul 2025 11:48:17 +0800 Subject: [PATCH] feat(core): bettery save mode (#12996) ## Summary by CodeRabbit * **New Features** * Introduced a battery save mode that can pause background sync and processing to conserve energy. * Battery save mode is automatically enabled when the app window loses focus and disabled when the window regains focus or is interacted with. * Available in both web and desktop (Electron) versions. * **Improvements** * Added user activity detection to optimize background operations for better battery efficiency. --- .../common/nbstore/src/sync/indexer/index.ts | 27 ++++++++++++++++ packages/common/nbstore/src/worker/client.ts | 22 +++++++++++++ .../common/nbstore/src/worker/consumer.ts | 31 +++++++++++++++++++ packages/common/nbstore/src/worker/ops.ts | 4 +++ .../src/app/effects/store-manager.ts | 9 ++++++ packages/frontend/apps/web/src/app.tsx | 9 ++++++ 6 files changed, 102 insertions(+) diff --git a/packages/common/nbstore/src/sync/indexer/index.ts b/packages/common/nbstore/src/sync/indexer/index.ts index 03a547172..1940c1dd0 100644 --- a/packages/common/nbstore/src/sync/indexer/index.ts +++ b/packages/common/nbstore/src/sync/indexer/index.ts @@ -159,6 +159,14 @@ export class IndexerSyncImpl implements IndexerSync { this.remote = Object.values(this.peers.remotes).find(remote => !!remote); } + enableBatterySaveMode() { + this.status.enableBatterySaveMode(); + } + + disableBatterySaveMode() { + this.status.disableBatterySaveMode(); + } + start() { if (this.abort) { this.abort.abort(MANUALLY_STOP); @@ -611,6 +619,10 @@ class IndexerSyncStatus { currentJob: string | null = null; errorMessage: string | null = null; statusUpdatedSubject$ = new Subject(); + batterySaveMode: { + promise: Promise; + resolve: () => void; + } | null = null; state$ = new Observable(subscribe => { const next = () => { @@ -685,6 +697,9 @@ class IndexerSyncStatus { } async acceptJob(abort?: AbortSignal) { + if (this.batterySaveMode) { + await this.batterySaveMode.promise; + } const job = await this.jobs.asyncPop(abort); this.currentJob = job; this.statusUpdatedSubject$.next(job); @@ -709,6 +724,18 @@ class IndexerSyncStatus { }; } + enableBatterySaveMode() { + if (this.batterySaveMode) { + return; + } + this.batterySaveMode = Promise.withResolvers(); + } + + disableBatterySaveMode() { + this.batterySaveMode?.resolve(); + this.batterySaveMode = null; + } + reset() { // reset all state, except prioritySettings this.isReadonly = false; diff --git a/packages/common/nbstore/src/worker/client.ts b/packages/common/nbstore/src/worker/client.ts index 931db21a6..aaae07e49 100644 --- a/packages/common/nbstore/src/worker/client.ts +++ b/packages/common/nbstore/src/worker/client.ts @@ -86,6 +86,21 @@ export class StoreManagerClient { connection.dispose(); }); } + + enableBatterySaveMode() { + this.connections.forEach(connection => { + connection.store.enableBatterySaveMode().catch(err => { + console.error('error enabling battery save mode', err); + }); + }); + } + disableBatterySaveMode() { + this.connections.forEach(connection => { + connection.store.disableBatterySaveMode().catch(err => { + console.error('error disabling battery save mode', err); + }); + }); + } } export class StoreClient { @@ -113,6 +128,13 @@ export class StoreClient { readonly blobFrontend: BlobFrontend; readonly awarenessFrontend: AwarenessFrontend; readonly indexerFrontend: IndexerFrontend; + + enableBatterySaveMode(): Promise { + return this.client.call('sync.enableBatterySaveMode'); + } + disableBatterySaveMode(): Promise { + return this.client.call('sync.disableBatterySaveMode'); + } } class WorkerDocStorage implements DocStorage { diff --git a/packages/common/nbstore/src/worker/consumer.ts b/packages/common/nbstore/src/worker/consumer.ts index 741a35696..600bdc531 100644 --- a/packages/common/nbstore/src/worker/consumer.ts +++ b/packages/common/nbstore/src/worker/consumer.ts @@ -134,6 +134,35 @@ class StoreConsumer { } } + private readonly ENABLE_BATTERY_SAVE_MODE_DELAY = 1000; + private enableBatterySaveModeTimeout: NodeJS.Timeout | null = null; + private enabledBatterySaveMode = false; + + enableBatterySaveMode() { + if (this.enableBatterySaveModeTimeout || this.enabledBatterySaveMode) { + return; + } + this.enableBatterySaveModeTimeout = setTimeout(() => { + if (!this.enabledBatterySaveMode) { + this.indexerSync.enableBatterySaveMode(); + this.enabledBatterySaveMode = true; + console.log('[BatterySaveMode] enabled'); + } + }, this.ENABLE_BATTERY_SAVE_MODE_DELAY); + } + + disableBatterySaveMode() { + if (this.enableBatterySaveModeTimeout) { + clearTimeout(this.enableBatterySaveModeTimeout); + this.enableBatterySaveModeTimeout = null; + } + if (this.enabledBatterySaveMode) { + this.indexerSync.disableBatterySaveMode(); + this.enabledBatterySaveMode = false; + console.log('[BatterySaveMode] disabled'); + } + } + private registerHandlers(consumer: OpConsumer) { const collectJobs = new Map< string, @@ -285,6 +314,8 @@ class StoreConsumer { this.indexerSync.search$(table, query, options), 'indexerSync.subscribeAggregate': ({ table, query, field, options }) => this.indexerSync.aggregate$(table, query, field, options), + 'sync.enableBatterySaveMode': () => this.enableBatterySaveMode(), + 'sync.disableBatterySaveMode': () => this.disableBatterySaveMode(), }); } } diff --git a/packages/common/nbstore/src/worker/ops.ts b/packages/common/nbstore/src/worker/ops.ts index 09bdc906a..21e7cb706 100644 --- a/packages/common/nbstore/src/worker/ops.ts +++ b/packages/common/nbstore/src/worker/ops.ts @@ -140,6 +140,10 @@ interface GroupedWorkerOps { AggregateResult, ]; }; + sync: { + enableBatterySaveMode: [void, void]; + disableBatterySaveMode: [void, void]; + }; } type Values = T extends { [k in keyof T]: any } ? T[keyof T] : never; diff --git a/packages/frontend/apps/electron-renderer/src/app/effects/store-manager.ts b/packages/frontend/apps/electron-renderer/src/app/effects/store-manager.ts index 884a63005..fbfb682a9 100644 --- a/packages/frontend/apps/electron-renderer/src/app/effects/store-manager.ts +++ b/packages/frontend/apps/electron-renderer/src/app/effects/store-manager.ts @@ -46,6 +46,15 @@ export function setupStoreManager(framework: Framework) { window.addEventListener('beforeunload', () => { storeManagerClient.dispose(); }); + window.addEventListener('focus', () => { + storeManagerClient.disableBatterySaveMode(); + }); + window.addEventListener('click', () => { + storeManagerClient.disableBatterySaveMode(); + }); + window.addEventListener('blur', () => { + storeManagerClient.enableBatterySaveMode(); + }); framework.impl(NbstoreProvider, { openStore(key, options) { diff --git a/packages/frontend/apps/web/src/app.tsx b/packages/frontend/apps/web/src/app.tsx index 8db03f263..74dbe3fe6 100644 --- a/packages/frontend/apps/web/src/app.tsx +++ b/packages/frontend/apps/web/src/app.tsx @@ -41,6 +41,15 @@ if ( window.addEventListener('beforeunload', () => { storeManagerClient.dispose(); }); +window.addEventListener('focus', () => { + storeManagerClient.disableBatterySaveMode(); +}); +window.addEventListener('click', () => { + storeManagerClient.disableBatterySaveMode(); +}); +window.addEventListener('blur', () => { + storeManagerClient.enableBatterySaveMode(); +}); const future = { v7_startTransition: true,