diff --git a/src/json-patch-ot/__tests__/scenarios.spec.ts b/src/json-patch-ot/__tests__/scenarios.spec.ts index 3b9d41bc64..795694e9ff 100644 --- a/src/json-patch-ot/__tests__/scenarios.spec.ts +++ b/src/json-patch-ot/__tests__/scenarios.spec.ts @@ -1013,6 +1013,50 @@ const groups: ScenarioGroup[] = [ }, ], }, + { + name: 'str_ins x str_del (same position)', + scenarios: [ + { + name: 'Deletes at same position as insert.', + docStart: {a: 'abcde'}, + user1: [{op: 'str_ins', path: '/a', pos: 0, str: 'X'}], + user2: [{op: 'str_del', path: '/a', pos: 0, str: 'abc'}], + docEnd: {a: 'Xde'}, + }, + { + name: 'Deletes at same non-zero position as insert.', + docStart: {a: '12abcde'}, + user1: [{op: 'str_ins', path: '/a', pos: 2, str: 'X'}], + user2: [{op: 'str_del', path: '/a', pos: 2, str: 'abc'}], + docEnd: {a: '12Xde'}, + }, + { + name: 'Deletes at higher position than insert.', + docStart: {a: 'hello world'}, + user1: [{op: 'str_ins', path: '/a', pos: 2, str: 'XX'}], + user2: [{op: 'str_del', path: '/a', pos: 8, str: 'rld'}], + docEnd: {a: 'heXXllo wo'}, + }, + ], + }, + { + name: 'str_ins x str_del (different paths)', + scenarios: [ + { + name: 'Operations on different paths should not interfere.', + docStart: { + a: 'hello', + b: 'world', + }, + user1: [{op: 'str_ins', path: '/a', pos: 0, str: 'X'}], + user2: [{op: 'str_del', path: '/b', pos: 0, str: 'wor'}], + docEnd: { + a: 'Xhello', + b: 'ld', + }, + }, + ], + }, /* { name: 'move x replace', diff --git a/src/json-patch-ot/transform.ts b/src/json-patch-ot/transform.ts index 6ef04b60b8..2422e7174e 100644 --- a/src/json-patch-ot/transform.ts +++ b/src/json-patch-ot/transform.ts @@ -6,10 +6,9 @@ import {xforms} from './transforms'; * already accepted patches. * * @param accepted Array of already accepted operations. - * @param proposed Array of proposed operations. Proposed operations are mutated inline. - * @param acceptedWins Whether accepted operation should win on when paths match exactly. + * @param proposed Array of proposed operations. * - * @returns Array of transformed changes + * @returns Array of transformed changes. */ export const transform = (accepted: readonly Op[], proposed: readonly Op[]): readonly Op[] => { const length = accepted.length; diff --git a/src/json-patch-ot/transforms/xStrDel.ts b/src/json-patch-ot/transforms/xStrDel.ts index 68f9a12c0d..2062c24f81 100644 --- a/src/json-patch-ot/transforms/xStrDel.ts +++ b/src/json-patch-ot/transforms/xStrDel.ts @@ -1,7 +1,9 @@ import {type Op, OpStrDel, OpStrIns} from '../../json-patch/op'; +import {isPathEqual} from '@jsonjoy.com/json-pointer'; export const xStrDel = (del: OpStrDel, op: Op): null | Op | Op[] => { if (op instanceof OpStrIns) { + if (!isPathEqual(del.path, op.path)) return op; const ins = op; if (ins.pos > del.pos) { const deleteLength = del.deleteLength(); @@ -11,6 +13,7 @@ export const xStrDel = (del: OpStrDel, op: Op): null | Op | Op[] => { } if (op instanceof OpStrDel) { + if (!isPathEqual(del.path, op.path)) return op; const opLen = op.deleteLength(); const delLen = del.deleteLength(); const overlapLen1 = del.pos + delLen - op.pos; diff --git a/src/json-patch-ot/transforms/xStrIns.ts b/src/json-patch-ot/transforms/xStrIns.ts index 17d2d3d3d0..9bd76e0a8a 100644 --- a/src/json-patch-ot/transforms/xStrIns.ts +++ b/src/json-patch-ot/transforms/xStrIns.ts @@ -1,11 +1,14 @@ import {operationToOp} from '../../json-patch/codec/json'; import {type Op, OpStrDel, OpStrIns} from '../../json-patch/op'; +import {isPathEqual} from '@jsonjoy.com/json-pointer'; export const xStrIns = (ins: OpStrIns, op: Op): null | Op | Op[] => { if (op instanceof OpStrIns) { + if (!isPathEqual(ins.path, op.path)) return op; if (ins.pos > op.pos) return op; return operationToOp({...op.toJson(), pos: op.pos + ins.str.length}, {}); } else if (op instanceof OpStrDel) { + if (!isPathEqual(ins.path, op.path)) return op; const del = op; if (del.pos < ins.pos) { const deleteLength: number = typeof del.str === 'string' ? del.str.length : del.len!; @@ -24,7 +27,7 @@ export const xStrIns = (ins: OpStrIns, op: Op): null | Op | Op[] => { } } } - if (ins.pos < del.pos) return operationToOp({...op.toJson(), pos: op.pos + ins.str.length}, {}); + if (ins.pos <= del.pos) return operationToOp({...op.toJson(), pos: op.pos + ins.str.length}, {}); return op; }