Group Hidden File Sync initialisation progress

This commit is contained in:
vorotamoroz
2026-07-23 16:47:30 +00:00
parent c8050322e4
commit 24b941f594
7 changed files with 433 additions and 43 deletions
@@ -54,11 +54,20 @@ import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import type { LiveSyncCore } from "@/main.ts";
import { tryGetFilePath } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
import {
configureHiddenFileSyncMode,
type ConfigureHiddenFileSyncResult,
} from "./configureHiddenFileSyncMode.ts";
import type { OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts";
import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlugins.ts";
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
type HiddenFileInitialisationProgress = {
log(message: string): void;
once(message: string): void;
done(message?: string): void;
};
const HIDDEN_FILE_NOTICE_GROUP = "hidden-file-changes";
const HIDDEN_FILE_NOTICE_DURATION_MS = 20_000;
@@ -1421,13 +1430,18 @@ Offline Changed files: ${files.length}`;
direction: SyncDirection,
showMessage: boolean,
// filesAll: InternalFileInfo[] | false = false,
targetFilesSrc: string[] | false = false
targetFilesSrc: string[] | false = false,
initialisationProgress?: HiddenFileInitialisationProgress
) {
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
const p = this._progress("[⚙ Initialise]\n", logLevel);
const p = initialisationProgress ?? this._progress("[⚙ Initialise]\n", logLevel);
// p.log("Resolving conflicts before starting...");
// await this.resolveConflictOnInternalFiles();
p.log("Initialising hidden files sync...");
// The initialisation progress owns the user-visible Notice. Its child
// rebuild and scan operations still write ordinary log entries, but
// must not each create another keep-alive Notice.
const showChildNotices = false;
// TODO: Handling ignore files cannot be performed to the hidden files.
const targetFiles = targetFilesSrc
@@ -1436,35 +1450,38 @@ Offline Changed files: ${files.length}`;
if (direction == "pushForce" || direction == "push") {
const onlyNew = direction == "push";
p.log(`Started: Storage --> Database ${onlyNew ? "(Only New)" : ""}`);
const updatedFiles = await this.rebuildFromStorage(showMessage, targetFiles, onlyNew);
const updatedFiles = await this.rebuildFromStorage(showChildNotices, targetFiles, onlyNew);
// making doubly sure, No more losing files.
// I did so many times during the development.
await this.adoptCurrentStorageFilesAsProcessed(updatedFiles);
await this.adoptCurrentDatabaseFilesAsProcessed(updatedFiles);
// And, scan other changes on the database (i.e. files which are on only other devices)
await this.scanAllStorageChanges(showMessage, true, false);
await this.scanAllDatabaseChanges(showMessage, true, false);
p.log("Checking for remaining storage and database changes...");
await this.scanAllStorageChanges(showChildNotices, true, false);
await this.scanAllDatabaseChanges(showChildNotices, true, false);
}
if (direction == "pullForce" || direction == "pull") {
const onlyNew = direction == "pull";
p.log(`Started: Database --> Storage ${onlyNew ? "(Only New)" : ""}`);
const updatedEntries = await this.rebuildFromDatabase(showMessage, targetFiles, onlyNew);
const updatedEntries = await this.rebuildFromDatabase(showChildNotices, targetFiles, onlyNew);
const updatedFiles = updatedEntries.map((e) => stripAllPrefixes(this.getPath(e)));
// making doubly sure, No more losing files.
await this.adoptCurrentStorageFilesAsProcessed(updatedFiles);
await this.adoptCurrentDatabaseFilesAsProcessed(updatedFiles);
// And, scan other changes on the database (i.e. files which are on only other devices)
await this.scanAllDatabaseChanges(showMessage, true, false);
await this.scanAllStorageChanges(showMessage, true, false);
p.log("Checking for remaining database and storage changes...");
await this.scanAllDatabaseChanges(showChildNotices, true, false);
await this.scanAllStorageChanges(showChildNotices, true, false);
}
if (direction == "safe") {
p.log(`Started: Database <--> Storage (by modified date)`);
const updatedFiles = await this.rebuildMerging(showMessage, targetFiles);
const updatedFiles = await this.rebuildMerging(showChildNotices, targetFiles);
await this.adoptCurrentStorageFilesAsProcessed(updatedFiles);
await this.adoptCurrentDatabaseFilesAsProcessed(updatedFiles);
// And, scan other changes on the database (i.e. files which are on only other devices)
await this.scanAllStorageChanges(showMessage, true, false);
await this.scanAllDatabaseChanges(showMessage, true, false);
p.log("Checking for remaining storage and database changes...");
await this.scanAllStorageChanges(showChildNotices, true, false);
await this.scanAllDatabaseChanges(showChildNotices, true, false);
}
p.done();
}
@@ -1789,32 +1806,44 @@ Offline Changed files: ${files.length}`;
}
async configureHiddenFileSync(mode: OptionalSyncFeatureMode) {
const result = await configureHiddenFileSyncMode(mode, {
disable: async () => {
// await this.core.$allSuspendExtraSync();
await this.core.services.setting.applyPartial(
{
syncInternalFiles: false,
},
true
);
// this.core.settings.syncInternalFiles = false;
// await this.core.saveSettings();
},
enable: async () => {
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
await this.core.services.setting.applyPartial(
{
useAdvancedMode: true,
syncInternalFiles: true,
},
true
);
},
initialise: async (direction) => {
await this.initialiseInternalFileSync(direction, true);
},
});
let initialisationProgress: HiddenFileInitialisationProgress | undefined;
let result: ConfigureHiddenFileSyncResult;
try {
result = await configureHiddenFileSyncMode(mode, {
disable: async () => {
// await this.core.$allSuspendExtraSync();
await this.core.services.setting.applyPartial(
{
syncInternalFiles: false,
},
true
);
// this.core.settings.syncInternalFiles = false;
// await this.core.saveSettings();
},
enable: async () => {
// Open the one user-visible progress Notice before saving
// the setting. Large Vaults can otherwise appear idle
// before the initial file enumeration begins.
initialisationProgress = this._progress("[⚙ Initialise]\n", LOG_LEVEL_NOTICE);
initialisationProgress.log("Preparing Hidden File Sync...");
await this.core.services.setting.applyPartial(
{
useAdvancedMode: true,
syncInternalFiles: true,
},
true
);
},
initialise: async (direction) => {
await this.initialiseInternalFileSync(direction, true, false, initialisationProgress);
initialisationProgress = undefined;
},
});
} catch (error) {
initialisationProgress?.done("Failed");
throw error;
}
if (result == "ignored" || result == "disabled") {
return;
}
@@ -1822,7 +1851,7 @@ Offline Changed files: ${files.length}`;
// this.plugin.settings.syncInternalFiles = true;
// await this.plugin.saveSettings();
this._log(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE);
this._log("Hidden File Sync initialisation completed.", LOG_LEVEL_INFO);
}
// <-- Configuration handling
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { LOG_LEVEL_NOTICE } from "@vrtmrz/livesync-commonlib/compat/common/types";
vi.mock("@/deps.ts", () => ({}));
vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({
@@ -21,6 +22,7 @@ vi.mock("./configureHiddenFileSyncMode.ts", () => ({
}));
import { HiddenFileSync } from "./CmdHiddenFileSync.ts";
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
describe("HiddenFileSync configuration-change notices", () => {
it("groups plug-in reloads and an Obsidian restart into one finished Notice", async () => {
@@ -102,4 +104,106 @@ describe("HiddenFileSync configuration-change notices", () => {
expect(core.services.appLifecycle.scheduleRestart).toHaveBeenCalledOnce();
expect(noticeGroups.removeItem).toHaveBeenCalledWith("hidden-file-changes", "restart");
});
it("keeps subordinate initialisation phases below Notice level so one progress Notice owns the scan", async () => {
const progress = {
log: vi.fn(),
once: vi.fn(),
done: vi.fn(),
};
const rebuildMerging = vi.fn(async () => []);
const adoptCurrentStorageFilesAsProcessed = vi.fn(async () => undefined);
const adoptCurrentDatabaseFilesAsProcessed = vi.fn(async () => undefined);
const scanAllStorageChanges = vi.fn(async () => undefined);
const scanAllDatabaseChanges = vi.fn(async () => undefined);
const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync;
Object.assign(hiddenFileSync, {
_progress: vi.fn(() => progress),
rebuildMerging,
adoptCurrentStorageFilesAsProcessed,
adoptCurrentDatabaseFilesAsProcessed,
scanAllStorageChanges,
scanAllDatabaseChanges,
});
await hiddenFileSync.initialiseInternalFileSync("safe", true);
expect(rebuildMerging).toHaveBeenCalledWith(false, false);
expect(scanAllStorageChanges).toHaveBeenCalledWith(false, true, false);
expect(scanAllDatabaseChanges).toHaveBeenCalledWith(false, true, false);
expect(progress.done).toHaveBeenCalledOnce();
});
it("does not surround the initialisation progress with separate gathering and restart Notices", async () => {
vi.mocked(configureHiddenFileSyncMode).mockImplementation(async (_mode, handlers) => {
await handlers.enable();
await handlers.initialise("safe");
return "enabled";
});
const events: string[] = [];
const progress = {
log: vi.fn((message: string) => {
events.push(`progress:${message}`);
}),
once: vi.fn(),
done: vi.fn(),
};
const createProgress = vi.fn(() => progress);
const applyPartial = vi.fn(async () => {
events.push("apply-settings");
});
const initialiseInternalFileSync = vi.fn(async () => undefined);
const log = vi.fn();
const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync;
Object.assign(hiddenFileSync, {
core: {
services: {
setting: { applyPartial },
},
},
initialiseInternalFileSync,
_progress: createProgress,
_log: log,
});
await hiddenFileSync.configureHiddenFileSync("MERGE");
expect(createProgress).toHaveBeenCalledWith("[⚙ Initialise]\n", LOG_LEVEL_NOTICE);
expect(events[0]).toBe("progress:Preparing Hidden File Sync...");
expect(initialiseInternalFileSync).toHaveBeenCalledWith("safe", true, false, progress);
expect(log).not.toHaveBeenCalledWith("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
expect(log).not.toHaveBeenCalledWith("Done! Restarting the app is strongly recommended!", LOG_LEVEL_NOTICE);
expect(log).toHaveBeenCalledWith("Hidden File Sync initialisation completed.", expect.any(Number));
});
it("closes the preparation Notice when enabling Hidden File Sync fails", async () => {
vi.mocked(configureHiddenFileSyncMode).mockImplementation(async (_mode, handlers) => {
await handlers.enable();
return "enabled";
});
const error = new Error("setting persistence failed");
const progress = {
log: vi.fn(),
once: vi.fn(),
done: vi.fn(),
};
const hiddenFileSync = Object.create(HiddenFileSync.prototype) as HiddenFileSync;
Object.assign(hiddenFileSync, {
core: {
services: {
setting: {
applyPartial: vi.fn(async () => {
throw error;
}),
},
},
},
_progress: vi.fn(() => progress),
_log: vi.fn(),
});
await expect(hiddenFileSync.configureHiddenFileSync("MERGE")).rejects.toBe(error);
expect(progress.done).toHaveBeenCalledWith("Failed");
});
});