Stop conflict checks and continuations after shutdown

This commit is contained in:
vorotamoroz committed 2026-11-04 08:19:25 +00:00
1 parent 6d811f80e1
commit d923510698
4 files changed
+389 -8

No files matched your search

@@ -25,6 +25,8 @@ export interface ConflictCheckingDependencies {
}
export interface ConflictCheckingHandlers {
/** Stop new checks and logging from pending checks after shutdown. */
readonly dispose: () => void;
readonly queueCheckForIfOpen: (file: FilePathWithPrefix) => Promise<void>;
readonly queueCheckFor: (file: FilePathWithPrefix) => Promise<void>;
readonly ensureAllProcessed: () => Promise<boolean>;
@@ -32,11 +34,14 @@ export interface ConflictCheckingHandlers {
/** Create conflict-checking handlers while retaining scheduling state privately. */
export function createConflictCheckingHandlers(dependencies: ConflictCheckingDependencies): ConflictCheckingHandlers {
let disposed = false;
const conflictQueue = createConflictCheckQueue({
async process(filename) {
if (disposed) return;
try {
await dependencies.conflict.resolve(filename);
} catch (error) {
if (disposed) return;
dependencies.log(`Conflict check failed: ${filename}`);
dependencies.log(error, LOG_LEVEL_VERBOSE);
}
@@ -45,11 +50,13 @@ export function createConflictCheckingHandlers(dependencies: ConflictCheckingDep
// and automatic merges can use the remaining execution slots.
concurrency: 10,
onProgress(remaining) {
// Accepted checks still update their accounting as they settle.
dependencies.conflictProcessQueueCount.value = remaining;
},
});
const queueCheckForIfOpen = async (file: FilePathWithPrefix): Promise<void> => {
if (disposed) return;
const path = file;
if (dependencies.currentSettings().checkConflictOnlyOnOpen) {
const activeFile: FilePath | undefined = dependencies.vault.getActiveFilePath();
@@ -62,7 +69,9 @@ export function createConflictCheckingHandlers(dependencies: ConflictCheckingDep
};
const queueCheckFor = async (file: FilePathWithPrefix): Promise<void> => {
if (disposed) return;
const optionalConflictResult = await dependencies.conflict.getOptionalConflictCheckMethod(file);
if (disposed) return;
if (optionalConflictResult == true) {
// The conflict has been resolved by another process.
return;
@@ -79,8 +88,12 @@ export function createConflictCheckingHandlers(dependencies: ConflictCheckingDep
};
const ensureAllProcessed = (): Promise<boolean> => conflictQueue.waitForIdle();
const dispose = (): void => {
disposed = true;
};
return {
dispose,
queueCheckForIfOpen,
queueCheckFor,
ensureAllProcessed,
@@ -14,7 +14,7 @@ import {
type MetaEntry,
type ObsidianLiveSyncSettings,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { EVENT_CONFLICT_CANCELLED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { EVENT_CONFLICT_CANCELLED, EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { NO_INTERACTION } from "@vrtmrz/livesync-commonlib/replication";
import type { IAPIService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
import {
@@ -44,6 +44,7 @@ type HarnessOptions = {
function createHarness(options: HarnessOptions = {}) {
const context = createServiceContext();
const conflict = new InjectableConflictService(context);
const unloadHandlers: Array<() => Promise<boolean>> = [];
const settings = { ...DEFAULT_SETTINGS, ...options.settings };
const tryAutoMerge = vi.fn();
const databaseFileAccess = {
@@ -62,7 +63,14 @@ function createHarness(options: HarnessOptions = {}) {
const activeFile = vi.fn(() => options.activeFile);
const services = {
API: { addLog },
appLifecycle: { isSuspended: vi.fn(() => false) },
appLifecycle: {
isSuspended: vi.fn(() => false),
onUnload: {
addHandler: vi.fn((handler: () => Promise<boolean>) => {
unloadHandlers.push(handler);
}),
},
},
conflict,
context,
database: { localDatabase: { tryAutoMerge } },
@@ -88,9 +96,22 @@ function createHarness(options: HarnessOptions = {}) {
serviceModules,
storageAccess,
tryAutoMerge,
unload: async () => {
for (const handler of unloadHandlers) await handler();
},
};
}
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
function leaf(rev: string, data: string, mtime: number): ConflictLeaf {
return { rev, data, mtime, ctime: mtime, deleted: false };
}
@@ -919,6 +940,306 @@ describe("conflict resolution serviceFeature", () => {
expect(harness.conflict.conflictProcessQueueCount.value).toBe(0);
});
it("settles queued checks without logging a resolver rejection after unload", async () => {
const path = "queued-rejection-after-unload.md" as FilePathWithPrefix;
const harness = createHarness();
const started = deferred<void>();
const pendingResolve = deferred<void>();
const resolve = vi.fn(async () => {
started.resolve(undefined);
await pendingResolve.promise;
});
harness.conflict.resolve.setHandler(resolve, true);
await harness.conflict.queueCheckFor(path);
await started.promise;
const completion = harness.conflict.ensureAllProcessed().then(
(result) => ({ status: "resolved" as const, result }),
(error: unknown) => ({ status: "rejected" as const, error })
);
await harness.unload();
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingResolve.reject(new Error("Resolver rejected after unload"));
await expect(completion).resolves.toEqual({ status: "resolved", result: true });
expect(resolve).toHaveBeenCalledOnce();
expect(harness.addLog).not.toHaveBeenCalled();
});
it("does not emit, log, or requeue a pending NOT_CONFLICTED result after the unload event", async () => {
const path = "not-conflicted-after-unload.md" as FilePathWithPrefix;
const harness = createHarness();
const queueCheckFor = vi.spyOn(harness.conflict, "queueCheckFor");
const started = deferred<void>();
const pendingMergeCheck = deferred<{ ok: typeof NOT_CONFLICTED }>();
harness.tryAutoMerge.mockImplementation(async () => {
started.resolve(undefined);
return await pendingMergeCheck.promise;
});
const emitEvent = vi.spyOn(harness.context.events, "emitEvent");
const operation = harness.conflict.resolve(path);
const completion = operation.then(
() => ({ status: "resolved" as const }),
(error: unknown) => ({ status: "rejected" as const, error })
);
await started.promise;
expect(harness.tryAutoMerge).toHaveBeenCalledWith(path, true);
harness.context.events.emitEvent(EVENT_PLUGIN_UNLOADED);
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingMergeCheck.resolve({ ok: NOT_CONFLICTED });
await expect(completion).resolves.toEqual({ status: "resolved" });
expect(emitEvent).not.toHaveBeenCalledWith(EVENT_CONFLICT_CANCELLED, path);
expect(queueCheckFor).not.toHaveBeenCalled();
expect(harness.addLog).not.toHaveBeenCalled();
});
it("does not delete a conflict or requeue when auto-merge storage completes after unload", async () => {
const path = "stored-merge-after-unload.md" as FilePathWithPrefix;
const harness = createHarness({ settings: { syncAfterMerge: false } });
const queueCheckFor = vi.spyOn(harness.conflict, "queueCheckFor");
const started = deferred<void>();
const pendingStore = deferred<boolean>();
harness.tryAutoMerge.mockResolvedValue({ result: "Merged content\n", conflictedRev: "2-conflict" });
harness.databaseFileAccess.storeContent.mockImplementation(async () => {
started.resolve(undefined);
return await pendingStore.promise;
});
const emitEvent = vi.spyOn(harness.context.events, "emitEvent");
const operation = harness.conflict.resolve(path);
const completion = operation.then(
() => ({ status: "resolved" as const }),
(error: unknown) => ({ status: "rejected" as const, error })
);
await started.promise;
expect(harness.databaseFileAccess.storeContent).toHaveBeenCalledWith(path, "Merged content\n");
await harness.unload();
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingStore.resolve(true);
await expect(completion).resolves.toEqual({ status: "resolved" });
expect(harness.fileHandler.deleteRevisionFromDB).not.toHaveBeenCalled();
expect(queueCheckFor).not.toHaveBeenCalled();
expect(emitEvent).not.toHaveBeenCalledWith(EVENT_CONFLICT_CANCELLED, path);
expect(harness.addLog).not.toHaveBeenCalled();
});
it("does not continue revision deletion after the unload event", async () => {
const path = "deleted-after-unload.md" as FilePathWithPrefix;
const harness = createHarness();
const started = deferred<void>();
const pendingDelete = deferred<boolean>();
harness.fileHandler.deleteRevisionFromDB.mockImplementation(async () => {
started.resolve(undefined);
return await pendingDelete.promise;
});
const emitEvent = vi.spyOn(harness.context.events, "emitEvent");
const operation = harness.conflict.resolveByDeletingRevision(path, "2-conflict", "UI Selected");
const completion = operation.then(
(result) => ({ status: "resolved" as const, result }),
(error: unknown) => ({ status: "rejected" as const, error })
);
await started.promise;
expect(harness.fileHandler.deleteRevisionFromDB).toHaveBeenCalledWith(path, "2-conflict");
harness.context.events.emitEvent(EVENT_PLUGIN_UNLOADED);
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingDelete.resolve(true);
await expect(completion).resolves.toMatchObject({ status: "resolved" });
expect(harness.databaseFileAccess.getConflictedRevs).not.toHaveBeenCalled();
expect(harness.fileHandler.dbToStorage).not.toHaveBeenCalled();
expect(emitEvent).not.toHaveBeenCalledWith(EVENT_CONFLICT_CANCELLED, path);
expect(harness.addLog).not.toHaveBeenCalled();
});
it("does not requeue or log after pending replication completes during unload", async () => {
const path = "replication-after-unload.md" as FilePathWithPrefix;
const harness = createHarness({ settings: { syncAfterMerge: true } });
const queueCheckFor = vi.spyOn(harness.conflict, "queueCheckFor");
const started = deferred<void>();
const pendingReplication = deferred<{ status: "completed" }>();
harness.tryAutoMerge.mockResolvedValue({ ok: AUTO_MERGED });
harness.services.replication.replicateUnattendedByEvent.mockImplementation(async () => {
started.resolve(undefined);
return await pendingReplication.promise;
});
const operation = harness.conflict.resolve(path);
const completion = operation.then(
() => ({ status: "resolved" as const }),
(error: unknown) => ({ status: "rejected" as const, error })
);
await started.promise;
expect(harness.services.replication.replicateUnattendedByEvent).toHaveBeenCalledWith({
trigger: "merge",
interaction: NO_INTERACTION,
});
await harness.unload();
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingReplication.resolve({ status: "completed" });
await expect(completion).resolves.toEqual({ status: "resolved" });
expect(queueCheckFor).not.toHaveBeenCalled();
expect(harness.addLog).not.toHaveBeenCalled();
});
it("does not start resolveByNewest when an optional conflict check returns newer after unload", async () => {
const path = "optional-newer-after-unload.md" as FilePathWithPrefix;
const harness = createHarness();
const started = deferred<void>();
const pendingOptionalCheck = deferred<"newer" | undefined>();
const optionalCheck = vi.fn(async () => {
started.resolve(undefined);
return await pendingOptionalCheck.promise;
});
const resolveByNewest = vi.fn(async () => true);
const unregisterOptionalCheck = harness.conflict.getOptionalConflictCheckMethod.addHandler(optionalCheck);
harness.conflict.resolveByNewest.setHandler(resolveByNewest, true);
const operation = harness.conflict.queueCheckFor(path);
const completion = operation.then(
() => ({ status: "resolved" as const }),
(error: unknown) => ({ status: "rejected" as const, error })
);
try {
await started.promise;
expect(optionalCheck).toHaveBeenCalledWith(path);
await harness.unload();
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingOptionalCheck.resolve("newer");
await expect(completion).resolves.toEqual({ status: "resolved" });
expect(resolveByNewest).not.toHaveBeenCalled();
expect(harness.addLog).not.toHaveBeenCalled();
} finally {
unregisterOptionalCheck();
}
});
it.each(["lifecycle", "event"] as const)(
"settles conflict operations without side effects after %s disposal",
async (trigger) => {
const path = "operation-after-unload.md" as FilePathWithPrefix;
const harness = createHarness();
const optionalCheck = vi.fn(async () => "newer" as const);
const unregisterOptionalCheck = harness.conflict.getOptionalConflictCheckMethod.addHandler(optionalCheck);
const publicOperations = [
vi.spyOn(harness.conflict, "queueCheckForIfOpen"),
vi.spyOn(harness.conflict, "queueCheckFor"),
vi.spyOn(harness.conflict, "resolve"),
vi.spyOn(harness.conflict, "resolveByDeletingRevision"),
vi.spyOn(harness.conflict, "resolveByNewest"),
vi.spyOn(harness.conflict, "resolveAllConflictedFilesByNewerOnes"),
];
const emitEvent = vi.spyOn(harness.context.events, "emitEvent");
if (trigger === "lifecycle") {
await harness.unload();
} else {
harness.context.events.emitEvent(EVENT_PLUGIN_UNLOADED);
}
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
try {
const results = await Promise.allSettled([
harness.conflict.queueCheckForIfOpen(path),
harness.conflict.queueCheckFor(path),
harness.conflict.resolve(path),
harness.conflict.resolveByDeletingRevision(path, "2-conflict", "UI Selected"),
harness.conflict.resolveByNewest(path),
harness.conflict.resolveAllConflictedFilesByNewerOnes(),
]);
expect(results.every(({ status }) => status === "fulfilled")).toBe(true);
for (const operation of publicOperations) expect(operation).toHaveBeenCalledOnce();
expect(optionalCheck).not.toHaveBeenCalled();
expect(harness.tryAutoMerge).not.toHaveBeenCalled();
expect(harness.databaseFileAccess.fetchEntryMeta).not.toHaveBeenCalled();
expect(harness.databaseFileAccess.getConflictedRevs).not.toHaveBeenCalled();
expect(harness.databaseFileAccess.storeContent).not.toHaveBeenCalled();
expect(harness.fileHandler.deleteRevisionFromDB).not.toHaveBeenCalled();
expect(harness.fileHandler.dbToStorage).not.toHaveBeenCalled();
expect(harness.storageAccess.getFileNames).not.toHaveBeenCalled();
expect(harness.services.replication.replicateUnattendedByEvent).not.toHaveBeenCalled();
expect(emitEvent).not.toHaveBeenCalledWith(EVENT_CONFLICT_CANCELLED, path);
expect(harness.addLog).not.toHaveBeenCalled();
} finally {
unregisterOptionalCheck();
}
}
);
it("does not start a queued resolver after active checks settle during unload", async () => {
const paths = Array.from({ length: 11 }, (_, index) => `unload-concurrent-${index}.md` as FilePathWithPrefix);
const harness = createHarness();
const tenStarted = deferred<void>();
const releaseResolvers = deferred<void>();
const resolve = vi.fn(async (_path: FilePathWithPrefix) => {
if (resolve.mock.calls.length === 10) tenStarted.resolve(undefined);
await releaseResolvers.promise;
});
harness.conflict.resolve.setHandler(resolve, true);
await Promise.all(paths.map(async (path) => await harness.conflict.queueCheckFor(path)));
await tenStarted.promise;
expect(harness.conflict.conflictProcessQueueCount.value).toBe(11);
const completion = harness.conflict.ensureAllProcessed();
harness.context.events.emitEvent(EVENT_PLUGIN_UNLOADED);
releaseResolvers.resolve(undefined);
await expect(completion).resolves.toBe(true);
expect(resolve).toHaveBeenCalledTimes(10);
expect(harness.conflict.conflictProcessQueueCount.value).toBe(0);
});
it("stops bulk conflict resolution when its storage scan completes after unload", async () => {
const path = "bulk-resolution-after-unload.md" as FilePathWithPrefix;
const harness = createHarness();
const namesStarted = deferred<void>();
const pendingNames = deferred<FilePathWithPrefix[]>();
harness.storageAccess.getFileNames.mockImplementation(async () => {
namesStarted.resolve(undefined);
return await pendingNames.promise;
});
const operation = harness.conflict.resolveAllConflictedFilesByNewerOnes();
await namesStarted.promise;
expect(harness.addLog).toHaveBeenCalled();
harness.addLog.mockClear();
await harness.unload();
harness.addLog.mockImplementation(() => {
throw new Error("Handler addLog not assigned");
});
pendingNames.resolve([path]);
await expect(operation).resolves.toBeUndefined();
expect(harness.databaseFileAccess.fetchEntryMeta).not.toHaveBeenCalled();
expect(harness.databaseFileAccess.getConflictedRevs).not.toHaveBeenCalled();
expect(harness.fileHandler.deleteRevisionFromDB).not.toHaveBeenCalled();
expect(harness.fileHandler.dbToStorage).not.toHaveBeenCalled();
expect(harness.addLog).not.toHaveBeenCalled();
});
it("drains requeued automatic checks when every resolver slot is occupied", async () => {
const harness = createHarness({ settings: { syncAfterMerge: false } });
const paths = Array.from({ length: 10 }, (_, index) => `requeue-${index}.md` as FilePathWithPrefix);
@@ -6,6 +6,7 @@ import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { createConflictCheckingHandlers } from "./checker";
import { createConflictResolutionOperations } from "./operations";
import { EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
export type ConflictResolutionHost = NecessaryServices<
"appLifecycle" | "conflict" | "database" | "replication" | "setting" | "vault",
@@ -47,6 +48,18 @@ export const useConflictResolutionFeature = createServiceFeature((host: Conflict
services.conflict.resolve.setHandler(operations.resolve);
services.conflict.resolveByNewest.setHandler(operations.resolveByNewest);
services.conflict.resolveAllConflictedFilesByNewerOnes.setHandler(operations.resolveAllConflictedFilesByNewerOnes);
const eventSubscriptions = new AbortController();
const dispose = (): void => {
checking.dispose();
operations.dispose();
eventSubscriptions.abort();
};
services.context.events.onceEvent(EVENT_PLUGIN_UNLOADED, dispose, { signal: eventSubscriptions.signal });
services.appLifecycle.onUnload.addHandler(() => {
dispose();
return Promise.resolve(true);
});
});
export { createConflictCheckingHandlers } from "./checker";
@@ -55,6 +55,8 @@ export interface ConflictResolutionOperationsDependencies {
}
export interface ConflictResolutionOperations {
/** Stop continuations without cancelling file or database operations already started. */
readonly dispose: () => void;
readonly cancelActiveResolution: (filename: FilePathWithPrefix) => void;
readonly resolveByDeletingRevision: (
path: FilePathWithPrefix,
@@ -68,13 +70,21 @@ export interface ConflictResolutionOperations {
readonly resolveAllConflictedFilesByNewerOnes: () => Promise<void>;
}
/** Create conflict-resolution operations which stop their continuations when disposed. */
export function createConflictResolutionOperations(
dependencies: ConflictResolutionOperationsDependencies
): ConflictResolutionOperations {
let disposed = false;
const latestResolveRequestByFilename = new Map<FilePathWithPrefix, number>();
let nextResolveRequestId = 0;
const dispose = (): void => {
disposed = true;
latestResolveRequestByFilename.clear();
};
const cancelActiveResolution = (filename: FilePathWithPrefix): void => {
if (disposed) return;
latestResolveRequestByFilename.delete(filename);
if (isLockAcquired(`conflict-resolve:${filename}`)) {
dependencies.events.emitEvent(EVENT_CONFLICT_CANCELLED, filename);
@@ -87,8 +97,11 @@ export function createConflictResolutionOperations(
subTitle = "",
showNotice = true
): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> => {
if (disposed) return MISSING_OR_ERROR;
const title = `Resolving ${subTitle ? `[${subTitle}]` : ""}:`;
if (!(await dependencies.fileHandler.deleteRevisionFromDB(path, deleteRevision))) {
const deleted = await dependencies.fileHandler.deleteRevisionFromDB(path, deleteRevision);
if (disposed) return MISSING_OR_ERROR;
if (!deleted) {
dependencies.log(
`${title} Could not delete conflicted revision ${displayRev(deleteRevision)} of ${path}`,
LOG_LEVEL_NOTICE
@@ -100,7 +113,9 @@ export function createConflictResolutionOperations(
`${title} Conflicted revision has been deleted ${displayRev(deleteRevision)} ${path}`,
LOG_LEVEL_INFO
);
if ((await dependencies.databaseFileAccess.getConflictedRevs(path)).length != 0) {
const conflicts = await dependencies.databaseFileAccess.getConflictedRevs(path);
if (disposed) return MISSING_OR_ERROR;
if (conflicts.length != 0) {
dependencies.log(`${title} some conflicts are left in ${path}`, LOG_LEVEL_INFO);
return AUTO_MERGED;
}
@@ -111,7 +126,9 @@ export function createConflictResolutionOperations(
// If no conflicts were found, write the resolved content to the storage.
const documentPath = parseDocumentPath(path);
const { path: filePath } = splitDocumentPath(documentPath);
if (!(await dependencies.fileHandler.dbToStorage(documentPath, filePath, true))) {
const reflected = await dependencies.fileHandler.dbToStorage(documentPath, filePath, true);
if (disposed) return MISSING_OR_ERROR;
if (!reflected) {
dependencies.log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
@@ -121,9 +138,11 @@ export function createConflictResolutionOperations(
};
const checkConflictAndPerformAutoMerge = async (path: FilePathWithPrefix): Promise<diff_check_result> => {
if (disposed) return CANCELLED;
const ret = await dependencies
.localDatabase()
.tryAutoMerge(path, !dependencies.currentSettings().disableMarkdownAutoMerge);
if (disposed) return CANCELLED;
if ("ok" in ret) {
return ret.ok;
}
@@ -131,7 +150,9 @@ export function createConflictResolutionOperations(
if ("result" in ret) {
const p = ret.result;
// 1. Store the merged content to the storage.
if (!(await dependencies.databaseFileAccess.storeContent(path, p))) {
const stored = await dependencies.databaseFileAccess.storeContent(path, p);
if (disposed) return CANCELLED;
if (!stored) {
dependencies.log(`Merged content cannot be stored:${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
@@ -186,6 +207,7 @@ export function createConflictResolutionOperations(
};
const resolve = async (filename: FilePathWithPrefix): Promise<void> => {
if (disposed) return;
const requestId = ++nextResolveRequestId;
latestResolveRequestByFilename.set(filename, requestId);
const serialisationKey = `conflict-resolve:${filename}`;
@@ -196,12 +218,12 @@ export function createConflictResolutionOperations(
dependencies.events.emitEvent(EVENT_CONFLICT_CANCELLED, filename);
}
return await withLock(serialisationKey, async () => {
if (latestResolveRequestByFilename.get(filename) !== requestId) {
if (disposed || latestResolveRequestByFilename.get(filename) !== requestId) {
return;
}
try {
const conflictCheckResult = await checkConflictAndPerformAutoMerge(filename);
if (latestResolveRequestByFilename.get(filename) !== requestId) {
if (disposed || latestResolveRequestByFilename.get(filename) !== requestId) {
return;
}
if (conflictCheckResult === NOT_CONFLICTED) {
@@ -223,6 +245,7 @@ export function createConflictResolutionOperations(
interaction: NO_INTERACTION,
});
}
if (disposed) return;
dependencies.log("[conflict] Automatically merged, but we have to check it again");
await dependencies.conflict.queueCheckFor(filename);
return;
@@ -249,12 +272,15 @@ export function createConflictResolutionOperations(
};
const resolveByNewest = async (filename: FilePathWithPrefix, showNotice = true): Promise<boolean> => {
if (disposed) return false;
const currentRev = await dependencies.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
if (disposed) return false;
if (currentRev == false) {
dependencies.log(`Could not get current revision of ${filename}`);
return Promise.resolve(false);
}
const revs = await dependencies.databaseFileAccess.getConflictedRevs(filename);
if (disposed) return false;
if (revs.length == 0) {
return Promise.resolve(true);
}
@@ -278,25 +304,31 @@ export function createConflictResolutionOperations(
}
return diff;
});
if (disposed) return false;
dependencies.log(
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
);
for (let i = 1; i < mTimeAndRev.length; i++) {
if (disposed) return false;
dependencies.log(
`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`
);
await resolveByDeletingRevision(filename, mTimeAndRev[i][1], "NEWEST", showNotice);
}
if (disposed) return false;
return true;
};
const resolveAllConflictedFilesByNewerOnes = async (): Promise<void> => {
if (disposed) return;
dependencies.log(`Resolving conflicts by newer ones`, LOG_LEVEL_NOTICE);
const files = await dependencies.storageAccess.getFileNames();
if (disposed) return;
let i = 0;
for (const file of files) {
if (disposed) return;
i++;
if (i % 10 === 0)
dependencies.log(
@@ -306,10 +338,12 @@ export function createConflictResolutionOperations(
);
await resolveByNewest(file, false);
}
if (disposed) return;
dependencies.log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
};
return {
dispose,
cancelActiveResolution,
resolveByDeletingRevision,
checkConflictAndPerformAutoMerge,