Keep incomplete Fast Setup isolated

This commit is contained in:
vorotamoroz
2026-08-07 08:27:04 +00:00
parent a9e64860d5
commit 15abe344bb
5 changed files with 385 additions and 21 deletions
+4 -7
View File
@@ -10,11 +10,7 @@ import {
synchroniseAllFilesBetweenDBandStorage,
type FullScanOptions,
} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
import {
adjustSettingToRemoteIfNeeded,
cancelScheduledInitialisation,
processVaultInitialisation,
} from "./redFlag";
import { adjustSettingToRemoteIfNeeded, cancelScheduledInitialisation, processVaultInitialisation } from "./redFlag";
export const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files";
export const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer";
@@ -217,7 +213,7 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
return await cancelScheduledInitialisation(host, cleanupFlag);
}
return await processVaultInitialisation(host, log, async () => {
const performFastSetup = async () => {
// 1. Perform fast DB fetch (download remote DB content to local DB)
await host.serviceModules.rebuilder.$fetchLocalDBFast(false);
@@ -253,5 +249,6 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
clearRememberedSimpleFetchMode(host);
log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE);
return true;
});
};
return await processVaultInitialisation(host, log, performFastSetup, "keep-on-failure");
}
+30 -9
View File
@@ -372,26 +372,35 @@ export async function cancelScheduledInitialisation(
return false;
}
type InitialisationSuspensionPolicy = "resume" | "keep" | "keep-on-failure";
/**
* Process vault initialisation with suspending file watching and sync.
* @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process.
* @param keepSuspending whether to keep suspending file watching after the process.
* @returns result of the process, or false if error occurs.
* Process Vault initialisation with file watching and synchronisation suspended.
* @param proc Process to execute during initialisation. It returns true only when normal operation may resume.
* @param suspensionPolicy Final file-reflection state. `keep-on-failure` controls both reflection directions so a partly completed Fast Setup remains isolated.
* @returns The result of the process, or false if an error occurs.
*/
export async function processVaultInitialisation(
host: NecessaryServices<"setting", never>,
log: LogFunction,
proc: () => Promise<boolean>,
keepSuspending = false
suspensionPolicy: InitialisationSuspensionPolicy = "resume"
) {
let completed = false;
try {
// Disable batch saving and file watching during initialisation.
await host.services.setting.applyPartial({ batchSave: false }, false);
await host.services.setting.suspendAllSync();
await host.services.setting.suspendExtraSync();
await host.services.setting.applyPartial({ suspendFileWatching: true }, true);
await host.services.setting.applyPartial(
suspensionPolicy === "keep-on-failure"
? { suspendFileWatching: true, suspendParseReplicationResult: true }
: { suspendFileWatching: true },
true
);
try {
const result = await proc();
completed = result;
return result;
} catch (ex) {
log("Error during vault initialisation process.", LOG_LEVEL_NOTICE);
@@ -403,9 +412,21 @@ export async function processVaultInitialisation(
log(ex, LOG_LEVEL_VERBOSE);
return false;
} finally {
if (!keepSuspending) {
// Re-enable file watching after initialisation.
if (suspensionPolicy === "resume") {
await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
} else if (suspensionPolicy === "keep") {
await host.services.setting.applyPartial({ suspendFileWatching: true }, true);
} else {
// Fast Setup owns both directions at this boundary. Reasserting the
// outcome also covers a late failure after finishRebuild started to
// resume reflection, and the legacy doNotSuspendOnFetching path.
await host.services.setting.applyPartial(
{
suspendFileWatching: !completed,
suspendParseReplicationResult: !completed,
},
true
);
}
}
}
@@ -512,7 +533,7 @@ export function createSuspendFlagHandler(
await host.services.setting.applyPartial({ writeLogToTheFile: true }, true);
return Promise.resolve(false);
},
true
"keep"
);
};
+78 -5
View File
@@ -325,13 +325,13 @@ describe("Red Flag Feature", () => {
() => {
return Promise.resolve(true);
},
false
"resume"
);
expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(false);
});
it("should keep suspending when keepSuspending is true", async () => {
it("should keep suspending when the policy is keep", async () => {
const host = createHostMock();
const log = createLoggerMock();
@@ -341,7 +341,7 @@ describe("Red Flag Feature", () => {
() => {
return Promise.resolve(true);
},
true
"keep"
);
expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(true);
@@ -357,7 +357,7 @@ describe("Red Flag Feature", () => {
() => {
throw new Error("Process failed");
},
false
"resume"
);
expect(result).toBe(false);
@@ -759,6 +759,79 @@ describe("Red Flag Feature", () => {
});
describe("askAndPerformFastSetupOnScheduledFetchAll", () => {
it("releases both reflection suspensions after Fast Setup succeeds", async () => {
const host = createHostMock();
const log = createLoggerMock();
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
Object.assign(host.mocks.setting.settings, {
doNotSuspendOnFetching: true,
suspendParseReplicationResult: true,
});
host.mocks.ui.confirm.confirmWithMessage
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_CLEANUP);
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({ batchSave: false }));
await expect(askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag)).resolves.toBe(true);
expect(host.mocks.setting.currentSettings()).toMatchObject({
suspendFileWatching: false,
suspendParseReplicationResult: false,
});
});
it("keeps Vault reflection suspended and preserves recovery state when Fast Fetch fails", 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(availableRemoteTweaks({ batchSave: false }));
host.mocks.rebuilder.$fetchLocalDBFast.mockRejectedValueOnce(new Error("cannot decrypt remote document"));
await expect(askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag)).resolves.toBe(false);
expect(host.mocks.setting.currentSettings()).toMatchObject({
suspendFileWatching: true,
suspendParseReplicationResult: true,
});
expect(host.mocks.rebuilder.finishRebuild).not.toHaveBeenCalled();
expect(cleanupFlag).not.toHaveBeenCalled();
expect(host.mocks.setting.deleteSmallConfig).not.toHaveBeenCalledWith("simple-fetch-mode");
});
it("re-suspends both reflection directions when finalisation fails after releasing them", 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(availableRemoteTweaks({ batchSave: false }));
host.mocks.rebuilder.finishRebuild.mockImplementationOnce(async () => {
await host.mocks.setting.applyPartial(
{
suspendFileWatching: false,
suspendParseReplicationResult: false,
},
true
);
throw new Error("Vault scan failed after reflection resumed");
});
await expect(askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag)).resolves.toBe(false);
expect(host.mocks.setting.currentSettings()).toMatchObject({
suspendFileWatching: true,
suspendParseReplicationResult: true,
});
expect(cleanupFlag).not.toHaveBeenCalled();
});
it("should remember quick flow choices while the scheduled fetch is pending", async () => {
const host = createHostMock();
const log = createLoggerMock();
@@ -1352,7 +1425,7 @@ describe("Red Flag Feature", () => {
() => {
return Promise.resolve(false);
},
true
"keep"
);
expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(true);