Refactor optional file synchronisation ownership

This commit is contained in:
vorotamoroz committed 2026-09-03 15:43:21 +00:00
1 parent 3b2d5aa5af
commit 826413bf84
67 files changed
+6294 -1856

No files matched your search

+15 -4
View File
@@ -1,15 +1,15 @@
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type ObsidianLiveSyncPlugin from "@/main";
import type { LiveSyncCore } from "@/main";
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
import {
StorageEventManagerBase,
type StorageEventManagerBaseDependencies,
} from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter";
export class StorageEventManagerObsidian extends StorageEventManagerBase<ObsidianStorageEventManagerAdapter> {
core: LiveSyncCore;
// Necessary evil.
// cmdHiddenFileSync: HiddenFileSync;
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) {
const adapter = new ObsidianStorageEventManagerAdapter(plugin, core, dependencies.fileProcessing);
super(adapter, dependencies);
@@ -22,6 +22,8 @@ export class StorageEventManagerObsidian extends StorageEventManagerBase<Obsidia
protected override async _watchVaultRawEvents(path: FilePath) {
if (!this.settings.syncInternalFiles && !this.settings.usePluginSync) return;
if (!this.settings.watchInternalFileChanges) return;
if (!this.settings.isConfigured || this.settings.suspendFileWatching) return;
if (this.settings.maxMTimeForReflectEvents > 0) return;
if (!path.startsWith(this.core.services.API.getSystemConfigDir())) return;
if (path.endsWith("/")) {
// Folder
@@ -30,6 +32,15 @@ export class StorageEventManagerObsidian extends StorageEventManagerBase<Obsidia
const isTargetFile = await this.vaultService.isTargetFileInExtra(path);
if (!isTargetFile) return;
// Commonlib's ordinary queue excludes every dot path when Hidden File
// Sync is disabled. A Customisation-only event has already passed the
// optional-file policy, so dispatch it without that unrelated gate.
if (!this.settings.syncInternalFiles) {
this.fileProcessing.onStorageFileEvent();
await this.fileProcessing.processOptionalFileEvent(path);
return;
}
void this.appendQueue(
[
{
@@ -0,0 +1,123 @@
import { describe, expect, it, vi } from "vitest";
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
vi.mock("@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager", () => ({
StorageEventManagerBase: class StorageEventManagerBase {
settings: unknown;
vaultService: unknown;
fileProcessing: unknown;
adapter: unknown;
appendQueue = vi.fn(async () => undefined);
constructor(adapter: unknown, dependencies: Record<string, unknown>) {
this.adapter = adapter;
this.settings = dependencies.settings;
this.vaultService = dependencies.vaultService;
this.fileProcessing = dependencies.fileProcessing;
}
},
}));
vi.mock("./ObsidianStorageEventManagerAdapter", () => ({
ObsidianStorageEventManagerAdapter: class ObsidianStorageEventManagerAdapter {
converter = {
toInternalFileInfo: vi.fn((path: FilePath) => ({ path, isInternal: true, isFolder: false })),
};
},
}));
import { StorageEventManagerObsidian } from "./StorageEventManagerObsidian.ts";
function createManager(
options: {
customisationEnabled?: boolean;
hiddenFileEnabled?: boolean;
target?: boolean;
configured?: boolean;
suspended?: boolean;
maxMTime?: number;
} = {}
) {
const settings = {
usePluginSync: options.customisationEnabled ?? true,
syncInternalFiles: options.hiddenFileEnabled ?? true,
watchInternalFileChanges: true,
isConfigured: options.configured ?? true,
suspendFileWatching: options.suspended ?? false,
maxMTimeForReflectEvents: options.maxMTime ?? 0,
};
const onStorageFileEvent = vi.fn();
const processOptionalFileEvent = vi.fn(async () => true);
const isTargetFileInExtra = vi.fn(async () => options.target ?? true);
const core = {
services: {
API: { getSystemConfigDir: vi.fn(() => ".obsidian") },
},
};
const manager = new StorageEventManagerObsidian(
{} as never,
core as never,
{
settings,
vaultService: { isTargetFileInExtra },
fileProcessing: { onStorageFileEvent, processOptionalFileEvent },
} as never
);
const runRawEvent = (path: string) =>
(manager as unknown as { _watchVaultRawEvents(path: FilePath): Promise<void> })._watchVaultRawEvents(
path as FilePath
);
return {
appendQueue: (manager as unknown as { appendQueue: ReturnType<typeof vi.fn> }).appendQueue,
isTargetFileInExtra,
onStorageFileEvent,
processOptionalFileEvent,
runRawEvent,
};
}
describe("StorageEventManagerObsidian optional-file raw events", () => {
it("dispatches a Customisation-only event without the base Hidden File Sync target gate", async () => {
const fixture = createManager({ hiddenFileEnabled: false });
await fixture.runRawEvent(".obsidian/app.json");
expect(fixture.processOptionalFileEvent).toHaveBeenCalledWith(".obsidian/app.json");
expect(fixture.onStorageFileEvent).toHaveBeenCalledOnce();
expect(fixture.appendQueue).not.toHaveBeenCalled();
});
it("retains the base queue for events while Hidden File Sync is enabled", async () => {
const fixture = createManager();
await fixture.runRawEvent(".obsidian/workspace");
expect(fixture.appendQueue).toHaveBeenCalledOnce();
expect(fixture.processOptionalFileEvent).not.toHaveBeenCalled();
});
it("does not dispatch disabled, filtered, folder, or out-of-directory paths", async () => {
const disabled = createManager({ customisationEnabled: false, hiddenFileEnabled: false });
await disabled.runRawEvent(".obsidian/app.json");
expect(disabled.appendQueue).not.toHaveBeenCalled();
const filtered = createManager({ target: false });
await filtered.runRawEvent(".obsidian/workspace");
await filtered.runRawEvent(".obsidian/folder/");
await filtered.runRawEvent("notes/example.md");
expect(filtered.appendQueue).not.toHaveBeenCalled();
expect(filtered.processOptionalFileEvent).not.toHaveBeenCalled();
});
it.each([
["unconfigured", { configured: false }],
["suspended", { suspended: true }],
["bounded by reflection time", { maxMTime: 1 }],
] as const)("preserves the base queue gate while %s", async (_label, options) => {
const fixture = createManager({ hiddenFileEnabled: false, ...options });
await fixture.runRawEvent(".obsidian/app.json");
expect(fixture.isTargetFileInExtra).not.toHaveBeenCalled();
expect(fixture.processOptionalFileEvent).not.toHaveBeenCalled();
});
});