/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/sync/conflictResolutionPlanner.ts
197 строк
7 KB
Robert Kuzhin
feat: add Obsidian session and desktop UX
09 авг 2026, 13:07
09 авг 2026, 13:07
14fe125
Код
Авторство
О чём код?
import type { Manifest, ManifestEntry } from "../mirror/manifestTypes.js"; import { validateManifest } from "../mirror/manifestValidation.js"; import type { VaultChange } from "../vault/vaultTypes.js"; import { ConflictManager } from "./conflictManager.js"; import { planReconciliation } from "./changePlanner.js"; import type { ConflictStrategy, PlannedAction, ReconciliationPlan, SyncConflict, } from "./syncTypes.js"; export interface ResolvedReconciliationPlan extends ReconciliationPlan { readonly mergedManifest: Manifest; readonly conflicts: readonly []; readonly vaultChanges: readonly VaultChange[]; } export function resolveReconciliation( base: Manifest, local: Manifest, remote: Manifest, strategies: readonly ConflictStrategy[], options: { readonly manager?: ConflictManager; readonly date?: Date; } = {}, ): ResolvedReconciliationPlan { const unresolved = planReconciliation(base, local, remote); if (unresolved.conflicts.length !== strategies.length) { throw new TypeError("Every synchronization conflict requires exactly one strategy"); } const manager = options.manager ?? new ConflictManager(); const conflictedIds = collectConflictedIds(unresolved.conflicts); const files = selectNonConflictingEntries(base, local, remote, conflictedIds); const actions: PlannedAction[] = unresolved.actions.filter( (action) => action.type !== "conflict", ); const vaultChanges: VaultChange[] = []; const occupiedPaths = new Set( [...Object.values(local.files), ...Object.values(remote.files)] .filter((entry) => entry.deleted !== true) .map((entry) => entry.path), ); for (const [index, conflict] of unresolved.conflicts.entries()) { const strategy = strategies[index]!; const resolution = manager.resolve( conflict, strategy, occupiedPaths, options.date ?? new Date(), ); actions.push(...resolution.actions); vaultChanges.push(...resolution.vaultChanges); for (const change of resolution.vaultChanges) { if (change.type === "rename") occupiedPaths.add(change.newPath); else if (change.type !== "delete") occupiedPaths.add(change.path); } for (const entry of selectedConflictEntries(conflict, strategy, resolution.actions)) { files[entry.fileId] = entry; } } const mergedManifest = validateManifest( { formatVersion: 1, vaultId: local.vaultId, revision: Math.max(local.revision, remote.revision) + 1, files, }, local.vaultId, ); return { actions, conflicts: [], mergedManifest, vaultChanges }; } function collectConflictedIds(conflicts: readonly SyncConflict[]): Set<string> { const result = new Set<string>(); for (const conflict of conflicts) { for (const fileId of [conflict.fileId, conflict.localFileId, conflict.remoteFileId]) { if (fileId !== undefined) result.add(fileId); } if (conflict.local !== undefined) result.add(conflict.local.fileId); if (conflict.remote !== undefined) result.add(conflict.remote.fileId); } return result; } function selectNonConflictingEntries( base: Manifest, local: Manifest, remote: Manifest, conflictedIds: ReadonlySet<string>, ): Record<string, ManifestEntry> { const files: Record<string, ManifestEntry> = {}; const ids = new Set([ ...Object.keys(base.files), ...Object.keys(local.files), ...Object.keys(remote.files), ]); for (const fileId of ids) { if (conflictedIds.has(fileId)) continue; const baseEntry = base.files[fileId]; const localEntry = local.files[fileId]; const remoteEntry = remote.files[fileId]; const selected = entriesEqual(localEntry, remoteEntry) ? localEntry : entriesEqual(localEntry, baseEntry) ? remoteEntry : localEntry; if (selected !== undefined) files[fileId] = selected; } return files; } function selectedConflictEntries( conflict: SyncConflict, strategy: ConflictStrategy, actions: readonly Exclude<PlannedAction, { readonly type: "conflict" }>[], ): ManifestEntry[] { const local = active(conflict.local); const remote = active(conflict.remote); if (strategy === "keep-both" && local === undefined) { return selectOneSide(conflict.remote, conflict.local); } if (strategy === "keep-both" && remote === undefined) { return selectOneSide(conflict.local, conflict.remote); } if (strategy === "keep-both" && local !== undefined && remote !== undefined) { const conflictPath = actions.find( ( action, ): action is Extract<PlannedAction, { readonly type: "decrypt-to-vault" }> => action.type === "decrypt-to-vault" && action.path !== local.path, )?.path; if (conflictPath === undefined) throw new TypeError("Keep-both resolution has no conflict path"); if (local.fileId !== remote.fileId) { return [local, { ...remote, path: conflictPath }]; } const copyAction = actions.find( (action) => action.type === "encrypt-from-vault" && action.fileId !== local.fileId && action.path === conflictPath, ); if (copyAction === undefined || copyAction.type !== "encrypt-from-vault") { throw new TypeError("Keep-both resolution has no distinct copy identity"); } return [ local, { ...remote, fileId: copyAction.fileId, path: conflictPath, objectVersion: 1, }, ]; } return strategy === "keep-remote" ? selectOneSide(conflict.remote, conflict.local) : selectOneSide(conflict.local, conflict.remote); } function selectOneSide( selected: ManifestEntry | undefined, rejected: ManifestEntry | undefined, ): ManifestEntry[] { const entries: ManifestEntry[] = []; if (selected !== undefined) entries.push(selected); if (selected === undefined && rejected !== undefined) entries.push({ ...rejected, deleted: true }); if ( selected !== undefined && rejected !== undefined && selected.fileId !== rejected.fileId ) { entries.push({ ...rejected, deleted: true }); } return entries; } function active(entry: ManifestEntry | undefined): ManifestEntry | undefined { return entry === undefined || entry.deleted === true ? undefined : entry; } function entriesEqual(left: ManifestEntry | undefined, right: ManifestEntry | undefined): boolean { if (left === right) return true; if (left === undefined || right === undefined) return false; return ( left.fileId === right.fileId && left.path === right.path && left.plaintextHash === right.plaintextHash && left.objectVersion === right.objectVersion && left.size === right.size && left.modifiedAt === right.modifiedAt && left.deleted === right.deleted ); }