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

@@ -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,