From bf8da52348ef700131c107cfe0948c21c8f77179 Mon Sep 17 00:00:00 2001 From: Ouyang Xingyuan Date: Wed, 24 Jun 2026 10:12:51 +0800 Subject: [PATCH] Remember pending Simple Fetch choices Simple Fetch asked the same mode and deletion choices again after a failed or interrupted pending Fetch All run. This stores the selected quick-flow choices in local small config until the operation completes, is cancelled, or is finalised. --- src/serviceFeatures/redFlag.simpleFetch.ts | 93 +++++++++++++++++----- src/serviceFeatures/redFlag.unit.spec.ts | 41 ++++++++++ 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/src/serviceFeatures/redFlag.simpleFetch.ts b/src/serviceFeatures/redFlag.simpleFetch.ts index 211b7dd..981556d 100644 --- a/src/serviceFeatures/redFlag.simpleFetch.ts +++ b/src/serviceFeatures/redFlag.simpleFetch.ts @@ -24,9 +24,73 @@ export const SIMPLE_FETCH_STAGE2_NEWER_CLEANUP = "Delete local files if deleted export const SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL = "Keep local files even if deleted on remote"; export const STAGE2_ABORT = "Cancel all and reboot"; +const SIMPLE_FETCH_MODE_KEY = "simple-fetch-mode"; + +function buildSimpleFetchResult(stage1: string, stage2?: string) { + if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { + return { mode: "legacy", options: {} }; + } + if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS && stage2) { + if (![SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL, SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE].includes(stage2)) { + return undefined; + } + return { + mode: "remote-only", + options: { + mode: FullScanModes.DB_APPLY, + extraOnRemote: + stage2 === SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL ? ExtraOnRemote.DELETE_LOCAL_MISSING : undefined, + }, + }; + } + if (stage1 === SIMPLE_FETCH_STAGE1_NEWER_WINS && stage2) { + if (![SIMPLE_FETCH_STAGE2_NEWER_CLEANUP, SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL].includes(stage2)) { + return undefined; + } + return { + mode: "newer-wins", + options: { + mode: FullScanModes.NEWER_WINS, + extraOnLocal: + stage2 === SIMPLE_FETCH_STAGE2_NEWER_CLEANUP + ? ExtraOnLocal.DELETE_DB_DELETED + : ExtraOnLocal.APPEND_STORAGE_ONLY, + }, + }; + } + return undefined; +} + +function rememberSimpleFetchMode(host: NecessaryServices<"setting", never>, stage1: string, stage2?: string) { + host.services.setting.setSmallConfig(SIMPLE_FETCH_MODE_KEY, JSON.stringify({ stage1, stage2 })); +} + +function getRememberedSimpleFetchMode(host: NecessaryServices<"setting", never>) { + const saved = host.services.setting.getSmallConfig(SIMPLE_FETCH_MODE_KEY); + if (!saved) return undefined; + try { + const { stage1, stage2 } = JSON.parse(saved) as { stage1?: string; stage2?: string }; + if (stage1) { + const remembered = buildSimpleFetchResult(stage1, stage2); + if (remembered) return remembered; + } + } catch { + // Clear below; the saved choice is optional and can be rebuilt by asking again. + } + host.services.setting.deleteSmallConfig(SIMPLE_FETCH_MODE_KEY); + return undefined; +} + +function clearRememberedSimpleFetchMode(host: NecessaryServices<"setting", never>) { + host.services.setting.deleteSmallConfig(SIMPLE_FETCH_MODE_KEY); +} + export async function askSimpleFetchMode( - host: NecessaryServices<"UI" | "vault", "storageAccess"> + host: NecessaryServices<"UI" | "setting", never> ): Promise<{ mode: string; options: Partial } | "cancelled" | "aborted"> { + const remembered = getRememberedSimpleFetchMode(host); + if (remembered) return remembered; + const msg = `We are about to retrieve the remote data. Firstly, how shall we handle the data retrieved from this remote server? @@ -55,7 +119,7 @@ Firstly, how shall we handle the data retrieved from this remote server? if (!stage1 || stage1 === SIMPLE_FETCH_STAGE1_CANCEL) return "cancelled"; if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { - return { mode: "legacy", options: {} }; + return buildSimpleFetchResult(stage1)!; } if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS) { @@ -77,14 +141,8 @@ Firstly, how shall we handle the data retrieved from this remote server? if (stage2 === STAGE2_ABORT) { return "aborted"; } - return { - mode: "remote-only", - options: { - mode: FullScanModes.DB_APPLY, - extraOnRemote: - stage2 === SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL ? ExtraOnRemote.DELETE_LOCAL_MISSING : undefined, - }, - }; + rememberSimpleFetchMode(host, stage1, stage2); + return buildSimpleFetchResult(stage1, stage2)!; } if (stage1 === SIMPLE_FETCH_STAGE1_NEWER_WINS) { @@ -107,16 +165,8 @@ Firstly, how shall we handle the data retrieved from this remote server? if (stage2 === STAGE2_ABORT) { return "aborted"; } - return { - mode: "newer-wins", - options: { - mode: FullScanModes.NEWER_WINS, - extraOnLocal: - stage2 === SIMPLE_FETCH_STAGE2_NEWER_CLEANUP - ? ExtraOnLocal.DELETE_DB_DELETED - : ExtraOnLocal.APPEND_STORAGE_ONLY, - }, - }; + rememberSimpleFetchMode(host, stage1, stage2); + return buildSimpleFetchResult(stage1, stage2)!; } return "cancelled"; @@ -143,12 +193,14 @@ export async function askAndPerformFastSetupOnScheduledFetchAll( const result = await askSimpleFetchMode(host); if (result === "cancelled") { log("Fetch cancelled by user.", LOG_LEVEL_NOTICE); + clearRememberedSimpleFetchMode(host); await cleanupFlag(); host.services.appLifecycle.performRestart(); return false; } if (result === "aborted") { log("Fetch exited by user.", LOG_LEVEL_NOTICE); + clearRememberedSimpleFetchMode(host); host.services.appLifecycle.performRestart(); return false; } @@ -191,6 +243,7 @@ export async function askAndPerformFastSetupOnScheduledFetchAll( } await host.serviceModules.rebuilder.finishRebuild(); await cleanupFlag(); + clearRememberedSimpleFetchMode(host); log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE); return true; }); diff --git a/src/serviceFeatures/redFlag.unit.spec.ts b/src/serviceFeatures/redFlag.unit.spec.ts index 7ab9e70..f94103e 100644 --- a/src/serviceFeatures/redFlag.unit.spec.ts +++ b/src/serviceFeatures/redFlag.unit.spec.ts @@ -85,6 +85,7 @@ const createSettingServiceMock = () => { writeLogToTheFile: false, remoteType: "CouchDB", }; + const smallConfig = new Map(); return { settings, currentSettings: vi.fn(() => settings), @@ -98,6 +99,13 @@ const createSettingServiceMock = () => { }), suspendAllSync: vi.fn(() => Promise.resolve()), suspendExtraSync: vi.fn(() => Promise.resolve()), + getSmallConfig: vi.fn((key: string) => smallConfig.get(key) ?? ""), + setSmallConfig: vi.fn((key: string, value: string) => { + smallConfig.set(key, value); + }), + deleteSmallConfig: vi.fn((key: string) => { + smallConfig.delete(key); + }), }; }; @@ -739,6 +747,39 @@ describe("Red Flag Feature", () => { }); describe("askAndPerformFastSetupOnScheduledFetchAll", () => { + it("should remember quick flow choices while the scheduled fetch is pending", async () => { + const host = createHostMock(); + const log = createLoggerMock(); + const cleanupFlag = vi.fn().mockResolvedValue(undefined); + + host.mocks.ui.confirm.confirmWithMessage + .mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS) + .mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_CLEANUP); + host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue({ batchSave: false } as any); + host.mocks.rebuilder.$fetchLocalDBFast.mockRejectedValueOnce(new Error("offline")); + + await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag); + await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag); + + expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(2); + expect(host.mocks.rebuilder.$fetchLocalDBFast).toHaveBeenCalledTimes(2); + }); + + it("should clear remembered quick flow choices after completion", async () => { + const host = createHostMock(); + const log = createLoggerMock(); + const cleanupFlag = vi.fn().mockResolvedValue(undefined); + + host.mocks.ui.confirm.confirmWithMessage + .mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS) + .mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL); + host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue({ batchSave: false } as any); + + await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag); + + expect(host.mocks.setting.deleteSmallConfig).toHaveBeenCalledWith("simple-fetch-mode"); + }); + it("should return false and cleanup when quick flow is cancelled", async () => { const host = createHostMock(); const log = createLoggerMock();