mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-21 18:17:05 +00:00
Refactor conflict resolution into service features
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
LOG_LEVEL_NOTICE,
|
||||
type FilePath,
|
||||
type FilePathWithPrefix,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IConflictService, IVaultService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
|
||||
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
|
||||
export type ConflictCheckingSettings = Pick<ObsidianLiveSyncSettings, "checkConflictOnlyOnOpen">;
|
||||
|
||||
export interface ConflictCheckingDependencies {
|
||||
readonly conflict: Pick<
|
||||
IConflictService,
|
||||
"getOptionalConflictCheckMethod" | "queueCheckFor" | "resolve" | "resolveByNewest"
|
||||
>;
|
||||
readonly conflictProcessQueueCount: ReactiveSource<number>;
|
||||
readonly currentSettings: () => ConflictCheckingSettings;
|
||||
readonly vault: Pick<IVaultService, "getActiveFilePath">;
|
||||
readonly log: LogFunction;
|
||||
}
|
||||
|
||||
export interface ConflictCheckingHandlers {
|
||||
readonly queueCheckForIfOpen: (file: FilePathWithPrefix) => Promise<void>;
|
||||
readonly queueCheckFor: (file: FilePathWithPrefix) => Promise<void>;
|
||||
readonly ensureAllProcessed: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create conflict-checking handlers and retain both queue processors privately.
|
||||
* The returned operations do not expose queue state to a host or consumer.
|
||||
*/
|
||||
export function createConflictCheckingHandlers(dependencies: ConflictCheckingDependencies): ConflictCheckingHandlers {
|
||||
const conflictResolveQueue = new QueueProcessor<FilePathWithPrefix, void>(
|
||||
async (filenames: FilePathWithPrefix[]) => {
|
||||
const filename = filenames[0];
|
||||
return await dependencies.conflict.resolve(filename);
|
||||
},
|
||||
{
|
||||
suspended: false,
|
||||
batchSize: 1,
|
||||
// No need to limit concurrency to `1` here, subsequent process will handle it,
|
||||
// and some cases do not need to be synchronised (for example, auto-merge).
|
||||
// Global concurrency is limited by the resolver with the UI.
|
||||
concurrentLimit: 10,
|
||||
delay: 0,
|
||||
keepResultUntilDownstreamConnected: false,
|
||||
}
|
||||
).replaceEnqueueProcessor((queue, newEntity) => {
|
||||
const newQueue = [...queue].filter((entry) => entry != newEntity);
|
||||
return [...newQueue, newEntity];
|
||||
});
|
||||
|
||||
const conflictCheckQueue = new QueueProcessor<FilePathWithPrefix, FilePathWithPrefix>(
|
||||
(files: FilePathWithPrefix[]) => {
|
||||
const filename = files[0];
|
||||
return Promise.resolve([filename]);
|
||||
},
|
||||
{
|
||||
suspended: false,
|
||||
batchSize: 1,
|
||||
concurrentLimit: 10,
|
||||
delay: 0,
|
||||
keepResultUntilDownstreamConnected: true,
|
||||
pipeTo: conflictResolveQueue,
|
||||
totalRemainingReactiveSource: dependencies.conflictProcessQueueCount,
|
||||
}
|
||||
);
|
||||
|
||||
const queueCheckForIfOpen = async (file: FilePathWithPrefix): Promise<void> => {
|
||||
const path = file;
|
||||
if (dependencies.currentSettings().checkConflictOnlyOnOpen) {
|
||||
const activeFile: FilePath | undefined = dependencies.vault.getActiveFilePath();
|
||||
if (activeFile && activeFile != path) {
|
||||
dependencies.log(`${file} is conflicted, merging process has been postponed.`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await dependencies.conflict.queueCheckFor(path);
|
||||
};
|
||||
|
||||
const queueCheckFor = async (file: FilePathWithPrefix): Promise<void> => {
|
||||
const optionalConflictResult = await dependencies.conflict.getOptionalConflictCheckMethod(file);
|
||||
if (optionalConflictResult == true) {
|
||||
// The conflict has been resolved by another process.
|
||||
return;
|
||||
} else if (optionalConflictResult === "newer") {
|
||||
// The conflict should be resolved by the newer entry.
|
||||
await dependencies.conflict.resolveByNewest(file);
|
||||
} else {
|
||||
conflictCheckQueue.enqueue(file);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureAllProcessed = (): Promise<boolean> => conflictResolveQueue.waitForAllProcessed();
|
||||
|
||||
return {
|
||||
queueCheckForIfOpen,
|
||||
queueCheckFor,
|
||||
ensureAllProcessed,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user