diff --git a/src/modules/coreFeatures/ModuleConflictChecker.ts b/src/modules/coreFeatures/ModuleConflictChecker.ts index fde5640..46d201b 100644 --- a/src/modules/coreFeatures/ModuleConflictChecker.ts +++ b/src/modules/coreFeatures/ModuleConflictChecker.ts @@ -37,13 +37,17 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule // TODO-> Move to ModuleConflictResolver? conflictResolveQueue = new QueueProcessor( async (filenames: FilePathWithPrefix[]) => { - await this.core.$$resolveConflict(filenames[0]); + const filename = filenames[0]; + return await this.core.$$resolveConflict(filename); }, { suspended: false, batchSize: 1, - concurrentLimit: 1, - delay: 10, + // No need to limit concurrency to `1` here, subsequent process will handle it, + // And, some cases, we do not need to synchronised. (e.g., auto-merge available). + // Therefore, limiting global concurrency is performed on resolver with the UI. + concurrentLimit: 10, + delay: 0, keepResultUntilDownstreamConnected: false, } ).replaceEnqueueProcessor((queue, newEntity) => { @@ -57,19 +61,13 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule new QueueProcessor( (files: FilePathWithPrefix[]) => { const filename = files[0]; - // const file = await this.core.storageAccess.isExists(filename); - // if (!file) return []; - // if (!(file instanceof TFile)) return; - // if ((file instanceof TFolder)) return []; - // Check again? return Promise.resolve([filename]); - // this.conflictResolveQueue.enqueueWithKey(filename, { filename, file }); }, { suspended: false, batchSize: 1, - concurrentLimit: 5, - delay: 10, + concurrentLimit: 10, + delay: 0, keepResultUntilDownstreamConnected: true, pipeTo: this.conflictResolveQueue, totalRemainingReactiveSource: this.core.conflictProcessQueueCount, diff --git a/src/modules/features/ModuleInteractiveConflictResolver.ts b/src/modules/features/ModuleInteractiveConflictResolver.ts index 8f2b4d5..c8a86ba 100644 --- a/src/modules/features/ModuleInteractiveConflictResolver.ts +++ b/src/modules/features/ModuleInteractiveConflictResolver.ts @@ -13,6 +13,7 @@ import { ConflictResolveModal } from "./InteractiveConflictResolving/ConflictRes import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts"; import { fireAndForget } from "octagonal-wheels/promises"; +import { serialized } from "../../lib/src/concurrency/lock.ts"; export class ModuleInteractiveConflictResolver extends AbstractObsidianModule implements IObsidianModule { $everyOnloadStart(): Promise { @@ -34,67 +35,71 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im } async $anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise { - this._log("Merge:open conflict dialog", LOG_LEVEL_VERBOSE); - const dialog = new ConflictResolveModal(this.app, filename, conflictCheckResult); - dialog.open(); - const selected = await dialog.waitForResult(); - if (selected === CANCELLED) { - // Cancelled by UI, or another conflict. - this._log(`Merge: Cancelled ${filename}`, LOG_LEVEL_INFO); - return false; - } - const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, true, true); - if (testDoc === false) { - this._log(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE); - return false; - } - if (!testDoc._conflicts) { - this._log(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE); - return false; - } - const toDelete = selected; - // const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev; - if (toDelete === LEAVE_TO_SUBSEQUENT) { - // Concatenate both conflicted revisions. - // Create a new file by concatenating both conflicted revisions. - const p = conflictCheckResult.diff.map((e) => e[1]).join(""); - const delRev = testDoc._conflicts[0]; - if (!(await this.core.databaseFileAccess.storeContent(filename, p))) { - this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE); + // UI for resolving conflicts should one-by-one. + return await serialized(`conflict-resolve-ui`, async () => { + this._log("Merge:open conflict dialog", LOG_LEVEL_VERBOSE); + const dialog = new ConflictResolveModal(this.app, filename, conflictCheckResult); + dialog.open(); + const selected = await dialog.waitForResult(); + if (selected === CANCELLED) { + // Cancelled by UI, or another conflict. + this._log(`Merge: Cancelled ${filename}`, LOG_LEVEL_INFO); return false; } - // 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage. - if ( - (await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) == - MISSING_OR_ERROR - ) { - this._log( - `Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`, - LOG_LEVEL_NOTICE - ); + const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, true, true); + if (testDoc === false) { + this._log(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE); return false; } - } else if (typeof toDelete === "string") { - // Select one of the conflicted revision to delete. - if ( - (await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) == MISSING_OR_ERROR - ) { + if (!testDoc._conflicts) { + this._log(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE); + return false; + } + const toDelete = selected; + // const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev; + if (toDelete === LEAVE_TO_SUBSEQUENT) { + // Concatenate both conflicted revisions. + // Create a new file by concatenating both conflicted revisions. + const p = conflictCheckResult.diff.map((e) => e[1]).join(""); + const delRev = testDoc._conflicts[0]; + if (!(await this.core.databaseFileAccess.storeContent(filename, p))) { + this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE); + return false; + } + // 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage. + if ( + (await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) == + MISSING_OR_ERROR + ) { + this._log( + `Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`, + LOG_LEVEL_NOTICE + ); + return false; + } + } else if (typeof toDelete === "string") { + // Select one of the conflicted revision to delete. + if ( + (await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) == + MISSING_OR_ERROR + ) { + this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); + return false; + } + } else { this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); return false; } - } else { - this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); + // In here, some merge has been processed. + // So we have to run replication if configured. + // TODO: Make this is as a event request + if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) { + await this.core.$$replicateByEvent(); + } + // And, check it again. + await this.core.$$queueConflictCheck(filename); return false; - } - // In here, some merge has been processed. - // So we have to run replication if configured. - // TODO: Make this is as a event request - if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) { - await this.core.$$replicateByEvent(); - } - // And, check it again. - await this.core.$$queueConflictCheck(filename); - return false; + }); } async allConflictCheck() { while (await this.pickFileForResolve()); diff --git a/src/modules/features/SettingDialogue/settingConstants.ts b/src/modules/features/SettingDialogue/settingConstants.ts index e2b016c..220d904 100644 --- a/src/modules/features/SettingDialogue/settingConstants.ts +++ b/src/modules/features/SettingDialogue/settingConstants.ts @@ -367,7 +367,7 @@ export const SettingInformation: Partial