Maintain watched file names for CLI collision checks

Share the CLI watcher with a reconciled name inventory, observe local
mutations, and retain newer notifications across overlapping reads.
Adopt the shared index for Obsidian and preserve fresh bulk operations.
This commit is contained in:
vorotamoroz committed 2026-11-12 14:04:17 +00:00
1 parent 78ebc2d298
commit 2a65fe0117
35 files changed
+5440 -28

No files matched your search

@@ -16,6 +16,7 @@ import {
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { EVENT_CONFLICT_CANCELLED, EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { NO_INTERACTION } from "@vrtmrz/livesync-commonlib/replication";
import type { FileNameReadOptions } from "@vrtmrz/livesync-commonlib/storage";
import type { IAPIService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
import {
createInstanceLogFunction,
@@ -58,7 +59,9 @@ function createHarness(options: HarnessOptions = {}) {
};
const addLog = vi.fn();
const storageAccess = {
getFileNames: vi.fn(async () => options.files ?? []),
getFileNames: vi.fn<(readOptions?: FileNameReadOptions) => Promise<FilePathWithPrefix[]>>(
async () => options.files ?? []
),
};
const activeFile = vi.fn(() => options.activeFile);
const services = {
@@ -262,6 +265,41 @@ describe("conflict resolution serviceFeature", () => {
expect(API.addLog).toHaveBeenLastCalledWith("Done!", LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
});
it("uses a fresh supported workset before bulk conflict revision deletion", async () => {
const harness = createHarness();
const currentPath = "current.md" as FilePathWithPrefix;
const removedPath = "removed-before-watch-delivery.md" as FilePathWithPrefix;
harness.storageAccess.getFileNames.mockImplementation((options) =>
Promise.resolve(options?.fresh ? [currentPath] : [removedPath])
);
harness.databaseFileAccess.fetchEntryMeta.mockImplementation((path: FilePathWithPrefix, rev?: string) =>
Promise.resolve(metadata(path, rev ?? "2-current", rev ? 1000 : 2000))
);
harness.databaseFileAccess.getConflictedRevs.mockResolvedValue(["1-older"]);
await harness.conflict.resolveAllConflictedFilesByNewerOnes();
expect(harness.fileHandler.deleteRevisionFromDB).toHaveBeenCalledExactlyOnceWith(currentPath, "1-older");
expect(harness.databaseFileAccess.fetchEntryMeta.mock.calls.every(([path]) => path === currentPath)).toBe(true);
expect(harness.storageAccess.getFileNames).toHaveBeenCalledExactlyOnceWith({ fresh: true });
});
it("does not delete conflict revisions when fresh bulk enumeration fails", async () => {
const harness = createHarness();
const failure = new Error("fresh listing could not validate its scope");
harness.storageAccess.getFileNames.mockImplementation((options) => {
if (options?.fresh) {
return Promise.reject(failure);
}
return Promise.resolve(["cached.md" as FilePathWithPrefix]);
});
await expect(harness.conflict.resolveAllConflictedFilesByNewerOnes()).rejects.toBe(failure);
expect(harness.databaseFileAccess.fetchEntryMeta).not.toHaveBeenCalled();
expect(harness.fileHandler.deleteRevisionFromDB).not.toHaveBeenCalled();
});
it("keeps resolver operations on narrow collaborators and extension seams", async () => {
const harness = createOperationsHarness();
const path = "same.md" as FilePathWithPrefix;
@@ -294,7 +294,10 @@ export function createConflictResolutionOperations(
const resolveAllConflictedFilesByNewerOnes = async (): Promise<void> => {
dependencies.log(`Resolving conflicts by newer ones`, LOG_LEVEL_NOTICE);
const files = await dependencies.storageAccess.getFileNames();
// This explicit bulk action deletes older revisions. Read the host's
// supported listing anew before choosing its workset; a listing failure
// must abort before any revision is removed.
const files = await dependencies.storageAccess.getFileNames({ fresh: true });
let i = 0;
for (const file of files) {
@@ -81,6 +81,8 @@ export async function findDiagnosticFiles(
reportProgress();
return cancelledResult(processed, candidates);
}
// The watched listing supplies a diagnostic existence hint. Membership here
// authorises no file or revision mutation, so a fresh full scan is unnecessary.
const vaultPaths = new Set(await core.storageAccess.getFileNames());
const matchingMetadataPaths = new Set<string>();
if (options.signal?.aborted) {
+2
View File
@@ -463,6 +463,8 @@ export async function copyFileDatabaseInfo(core: FileDatabaseInfoCore, path: str
export async function collectFileDatabaseInfoPaths(core: FileDatabaseInfoCore): Promise<string[]> {
const ignorePatterns = getFileRegExp(core.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(core.settings, "syncInternalFilesTargetPatterns");
// These names seed a chooser or inspection; per-entry reads establish current
// state before offering actions. Hidden-file mode keeps its separate listing scope.
const storagePaths = core.settings.syncInternalFiles
? await core.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns)
: await core.storageAccess.getFileNames();