import { ExtendedPatch, ExtendedPathKey, applyExtendedPatch } from "./extended"; import type { HistoryEntry } from "./stateManager"; export namespace HistoryEntries { export function merge(parameters: { previous: HistoryEntry; next: HistoryEntry; mergeMetadata?: (parameters: { previous: M; next: M }) => M; }): HistoryEntry { const { previous, next, mergeMetadata } = parameters; const metadata = mergeMetadata ? mergeMetadata({ previous: previous.metadata, next: next.metadata }) : next.metadata; // If we don't have patches, fall back to a simple merge where we do both operations if ( !next.undoPatches || !next.redoPatches || !previous.undoPatches || !previous.redoPatches ) { return { metadata, undo: (s) => { s = next.undo(s); s = previous.undo(s); return s; }, redo: (s) => { s = previous.redo(s); s = next.redo(s); return s; }, }; } const undoPatches = compactPatches([ ...next.undoPatches, ...previous.undoPatches, ]); const redoPatches = compactPatches([ ...previous.redoPatches, ...next.redoPatches, ]); return { metadata, undo: (s) => applyExtendedPatch(s, undoPatches), redo: (s) => applyExtendedPatch(s, redoPatches), undoPatches, redoPatches, }; } export function mergeOperations(parameters: { previous: ExtendedPatch; next: ExtendedPatch; }): ExtendedPatch | "conflict" | "ok" { const { previous, next } = parameters; // If either is a move operation, we can't merge if (previous.op === "move" || next.op === "move") { return "conflict"; } if (pathsEqual(previous.path, next.path)) { return next; } return "ok"; } export function compactPatches(patches: ExtendedPatch[]) { let result: ExtendedPatch[] = [...patches]; for (let i = 0; i < result.length; i++) { const patch = result[i]; for (let j = i + 1; j < result.length; j++) { const next = result[j]; const merged = mergeOperations({ previous: patch, next }); if (merged === "conflict") { break; } else if (merged === "ok") { continue; } else { result[j] = merged; result.splice(i, 1); i--; break; } } } return result; } export function pathsEqual(a: ExtendedPathKey[], b: ExtendedPathKey[]) { // Check length first if (a.length !== b.length) { return false; } return a.every((path, index) => { const other = b[index]; if ( typeof path === "object" && "id" in path && typeof other === "object" && "id" in other ) { return path.id === other.id; } return path === other; }); } }