Merge pull request #978 from apple-ouyang/codex/fast-fetch-interruptions

Remember Simple Fetch choices during interrupted initialisation
This commit is contained in:
vorotamoroz
2026-06-29 19:56:17 +09:00
committed by GitHub
3 changed files with 115 additions and 21 deletions
+1 -1
Submodule src/lib updated: 0563f267ec...17c8c245bb
+73 -20
View File
@@ -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 SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL = "Keep local files even if deleted on remote";
export const STAGE2_ABORT = "Cancel all and reboot"; 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( export async function askSimpleFetchMode(
host: NecessaryServices<"UI" | "vault", "storageAccess"> host: NecessaryServices<"UI" | "setting", never>
): Promise<{ mode: string; options: Partial<FullScanOptions> } | "cancelled" | "aborted"> { ): Promise<{ mode: string; options: Partial<FullScanOptions> } | "cancelled" | "aborted"> {
const remembered = getRememberedSimpleFetchMode(host);
if (remembered) return remembered;
const msg = `We are about to retrieve the remote data. const msg = `We are about to retrieve the remote data.
Firstly, how shall we handle the data retrieved from this remote server? 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 || stage1 === SIMPLE_FETCH_STAGE1_CANCEL) return "cancelled";
if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) {
return { mode: "legacy", options: {} }; return buildSimpleFetchResult(stage1)!;
} }
if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS) { 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) { if (stage2 === STAGE2_ABORT) {
return "aborted"; return "aborted";
} }
return { rememberSimpleFetchMode(host, stage1, stage2);
mode: "remote-only", return buildSimpleFetchResult(stage1, stage2)!;
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) { 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) { if (stage2 === STAGE2_ABORT) {
return "aborted"; return "aborted";
} }
return { rememberSimpleFetchMode(host, stage1, stage2);
mode: "newer-wins", return buildSimpleFetchResult(stage1, stage2)!;
options: {
mode: FullScanModes.NEWER_WINS,
extraOnLocal:
stage2 === SIMPLE_FETCH_STAGE2_NEWER_CLEANUP
? ExtraOnLocal.DELETE_DB_DELETED
: ExtraOnLocal.APPEND_STORAGE_ONLY,
},
};
} }
return "cancelled"; return "cancelled";
@@ -143,12 +193,14 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
const result = await askSimpleFetchMode(host); const result = await askSimpleFetchMode(host);
if (result === "cancelled") { if (result === "cancelled") {
log("Fetch cancelled by user.", LOG_LEVEL_NOTICE); log("Fetch cancelled by user.", LOG_LEVEL_NOTICE);
clearRememberedSimpleFetchMode(host);
await cleanupFlag(); await cleanupFlag();
host.services.appLifecycle.performRestart(); host.services.appLifecycle.performRestart();
return false; return false;
} }
if (result === "aborted") { if (result === "aborted") {
log("Fetch exited by user.", LOG_LEVEL_NOTICE); log("Fetch exited by user.", LOG_LEVEL_NOTICE);
clearRememberedSimpleFetchMode(host);
host.services.appLifecycle.performRestart(); host.services.appLifecycle.performRestart();
return false; return false;
} }
@@ -191,6 +243,7 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
} }
await host.serviceModules.rebuilder.finishRebuild(); await host.serviceModules.rebuilder.finishRebuild();
await cleanupFlag(); await cleanupFlag();
clearRememberedSimpleFetchMode(host);
log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE); log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE);
return true; return true;
}); });
+41
View File
@@ -85,6 +85,7 @@ const createSettingServiceMock = () => {
writeLogToTheFile: false, writeLogToTheFile: false,
remoteType: "CouchDB", remoteType: "CouchDB",
}; };
const smallConfig = new Map<string, string>();
return { return {
settings, settings,
currentSettings: vi.fn(() => settings), currentSettings: vi.fn(() => settings),
@@ -98,6 +99,13 @@ const createSettingServiceMock = () => {
}), }),
suspendAllSync: vi.fn(() => Promise.resolve()), suspendAllSync: vi.fn(() => Promise.resolve()),
suspendExtraSync: 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", () => { 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 () => { it("should return false and cleanup when quick flow is cancelled", async () => {
const host = createHostMock(); const host = createHostMock();
const log = createLoggerMock(); const log = createLoggerMock();