mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-14 20:20:12 +00:00
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:
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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user