git-documentdb
Version:
Offline-first database that syncs with Git
466 lines • 22.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonPatchOT = void 0;
/* eslint-disable unicorn/prefer-spread */
/* eslint-disable max-depth */
const ot_json1_1 = require("ot-json1");
const unicount_1 = require("unicount");
const const_1 = require("../const");
class JsonPatchOT {
constructor(options) {
options !== null && options !== void 0 ? options : (options = {
keyOfUniqueArray: undefined,
});
this._keyOfUniqueArray =
options.keyOfUniqueArray !== undefined ? options.keyOfUniqueArray : [];
}
_textCreateOp(path, startNum, str) {
if (startNum > 0) {
return (0, ot_json1_1.editOp)(path, 'text-unicode', [startNum, str]);
}
return (0, ot_json1_1.editOp)(path, 'text-unicode', [str]);
}
_textReplaceOp(path, startNum, from, to) {
if (startNum > 0) {
return (0, ot_json1_1.editOp)(path, 'text-unicode', [startNum, { d: (0, unicount_1.uniCount)(from) }, to]);
}
return (0, ot_json1_1.editOp)(path, 'text-unicode', [{ d: (0, unicount_1.uniCount)(from) }, to]);
}
_textDeleteOp(path, startNum, str) {
if (startNum > 0) {
return (0, ot_json1_1.editOp)(path, 'text-unicode', [startNum, { d: (0, unicount_1.uniCount)(str) }]);
}
return (0, ot_json1_1.editOp)(path, 'text-unicode', [{ d: (0, unicount_1.uniCount)(str) }]);
}
// eslint-disable-next-line complexity
getTextOp(path, text) {
// From text patch
const operators = [];
const lines = text.split('\n');
let startNum = 0;
let currentLine = 0;
for (; currentLine < lines.length; currentLine++) {
const line = decodeURI(lines[currentLine]);
let patchStart = line.match(/^@@ -(\d+?),\d+? \+\d+?,\d+? @@/);
if (!patchStart)
patchStart = line.match(/^@@ -(\d+?) \+\d+?,\d+? @@/m);
if (!patchStart)
patchStart = line.match(/^@@ -(\d+?),\d+? \+\d+? @@/m);
if (patchStart) {
startNum = parseInt(patchStart[1], 10) - 1;
continue;
}
// Need s option to allow . to match newline characters.
const isContextLine = line.match(/^ (.+?)$/s);
if (isContextLine) {
const context = isContextLine[1];
startNum += (0, unicount_1.uniCount)(context);
continue;
}
// Need s option to allow . to match newline characters.
const isAddOrDeleteLine = line.match(/^([+-])(.+?)$/s);
if (isAddOrDeleteLine) {
const addOrDelete = isAddOrDeleteLine[1];
const str = isAddOrDeleteLine[2];
if (addOrDelete === '+') {
// Create
operators.push(this._textCreateOp(path, startNum, str));
startNum += (0, unicount_1.uniCount)(str);
continue;
}
// addOrDelete is '-'
let isReplace = false;
// Read next line to check replace text
if (currentLine + 1 < lines.length) {
const nextLine = decodeURI(lines[currentLine + 1]);
// Need s option to allow . to match newline characters.
const isReplaceLine = nextLine.match(/^\+(.+?)$/s);
if (isReplaceLine) {
isReplace = true;
const replaceTo = isReplaceLine[1];
operators.push(this._textReplaceOp(path, startNum, str, replaceTo));
currentLine++;
startNum += (0, unicount_1.uniCount)(replaceTo) - (0, unicount_1.uniCount)(str);
}
}
if (!isReplace) {
// Delete
operators.push(this._textDeleteOp(path, startNum, str));
startNum -= (0, unicount_1.uniCount)(str);
}
}
}
return operators.reduce(ot_json1_1.type.compose, null);
}
fromDiff(diff) {
if (diff === undefined)
return null; // Do not be undefined. Use null.
const operations = [];
const procTree = (ancestors, tree) => {
const keys = Object.keys(tree);
let sortedKeys = [];
const isArray = keys.includes('_t');
const nest = [];
const insOp = [];
const replOp = [];
const remOp = [];
const movOp = [];
const txtOp = [];
if (isArray) {
// is Array
// underscore _ means 'remove' or 'move' operation
keys.sort(); // 1, 2, 3, _1, _2, _3
// console.log('## start parse keys: ' + JSON.stringify(keys));
let underBarStart = 0;
for (underBarStart = 0; underBarStart < keys.length; underBarStart++) {
if (keys[underBarStart].startsWith('_')) {
break;
}
}
// no bar: insert/replace/text
keys.slice(0, underBarStart).forEach(key => {
if (!Array.isArray(tree[key]))
nest.push(key);
else if (tree[key].length === 1)
insOp.push(key);
else if (tree[key].length === 2)
replOp.push(key);
else if (tree[key].length === 3 && typeof tree[key][0] === 'string')
txtOp.push(key);
});
// underBar: remove/move
// eslint-disable-next-line complexity
keys.slice(underBarStart, keys.length).forEach(key => {
if (!Array.isArray(tree[key]))
nest.push(key);
else if (tree[key].length === 3) {
const arr = tree[key];
if (arr[1] === 0 && arr[2] === 0)
remOp.push(key);
else if (arr[0] === '' && arr[2] === 3)
movOp.push(key);
}
});
movOp.sort((a, b) =>
// sort by destination position
tree[a][1] - tree[b][1]);
insOp.sort((a, b) =>
// sort by destination position
parseInt(a, 10) - parseInt(b, 10));
// sort order: replace, text, remove, insert, move
sortedKeys = sortedKeys.concat(replOp, txtOp, remOp, movOp, insOp, nest);
}
else {
// is Object
sortedKeys = keys.sort();
}
const removedIndex = [];
const insertedIndex = [];
const movedOperation = [];
// eslint-disable-next-line complexity
sortedKeys.forEach(key => {
// console.log('# ' + key);
if (Array.isArray(tree[key])) {
const arr = tree[key];
if (arr.length === 1) {
// Insert
if (isArray && typeof key === 'string') {
key = parseInt(key.replace(/^_/, ''), 10); // Remove heading underscore
}
if (isArray) {
let dstOffset = 0;
movOp.forEach(mop => {
const from = parseInt(mop, 10);
const to = tree[mop][1];
if (from < key && to > key)
dstOffset++;
if (from > key && to < key)
dstOffset--;
});
operations.push((0, ot_json1_1.insertOp)(ancestors.concat(key + dstOffset), arr[0]));
insertedIndex.push(key);
}
else {
operations.push((0, ot_json1_1.insertOp)(ancestors.concat(key), arr[0]));
}
}
else if (arr.length === 2) {
// Replace
if (isArray && typeof key === 'string') {
key = parseInt(key.replace(/^_/, ''), 10); // Remove heading underscore
}
operations.push((0, ot_json1_1.replaceOp)(ancestors.concat(key), arr[0], arr[1]));
}
else if (arr.length === 3) {
const firstItem = arr[0];
if (isArray && typeof key === 'string') {
key = parseInt(key.replace(/^_/, ''), 10); // Remove heading underscore
}
if (arr[1] === 0 && arr[2] === 0) {
// Remove
// See https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md
if (typeof key === 'string') {
// Remove property
operations.push((0, ot_json1_1.removeOp)(ancestors.concat(key)));
}
else {
// Remove from array
const offset = -removedIndex.length;
operations.push((0, ot_json1_1.removeOp)(ancestors.concat(key + offset)));
removedIndex.push(key);
}
}
else if (arr[0] === '' && arr[2] === 3) {
// Moved
// See https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md
if (isArray) {
let offset = 0;
removedIndex.forEach(index => {
if (parseInt(index, 10) <= parseInt(key, 10))
offset--;
});
/*
insertedIndex.forEach(index => {
if (parseInt(index as string, 10) <= parseInt(key as string, 10))
offset++;
});
*/
movedOperation.forEach(mop => {
if (mop.from < key && mop.to > arr[1])
offset--;
if (mop.from > key && mop.to < arr[1])
offset++;
});
let dstOffset = 0;
insOp.forEach(iop => {
if (parseInt(iop, 10) <= parseInt(arr[1], 10))
dstOffset--;
});
operations.push((0, ot_json1_1.moveOp)(ancestors.concat(key + offset), ancestors.concat(arr[1] + dstOffset)));
movedOperation.push({ from: key, to: arr[1] });
}
else {
operations.push((0, ot_json1_1.moveOp)(ancestors.concat(key), ancestors.concat(arr[1])));
}
}
else if (typeof firstItem === 'string') {
let isTextPatch = firstItem.match(/^@@ -\d+?,\d+? \+\d+?,\d+? @@\n/m);
if (!isTextPatch)
isTextPatch = firstItem.match(/^@@ -\d+? \+\d+?,\d+? @@\n/m);
if (!isTextPatch)
isTextPatch = firstItem.match(/^@@ -\d+?,\d+? \+\d+? @@\n/m);
if (isTextPatch) {
const textOp = this.getTextOp(ancestors.concat(key), firstItem);
if (textOp) {
operations.push(textOp);
}
}
}
}
}
else if (typeof tree[key] === 'object') {
if (isArray && typeof key === 'string') {
key = parseInt(key.replace(/^_/, ''), 10); // Remove heading underscore
}
procTree(ancestors.concat(key), tree[key]);
}
});
};
procTree([], diff);
if (operations.length === 1) {
return operations[0];
}
/**
* A path can be a flat array format in the specification. e.g.) ['x', 'y', {i:2}]
* https://github.com/ottypes/json1/blob/master/spec.md
* However type.transform function does not accept the flat array format.
* Use a nested array format instead. e.g.) ['x', ['y', {i:2}]].
* type.compose converts the flat array format to the nested array format.
*/
const reducedOperations = operations.reduce(ot_json1_1.type.compose, null);
return reducedOperations;
}
apply(doc, op) {
return ot_json1_1.type.apply(doc, op);
}
// eslint-disable-next-line complexity
patch(docOurs, diffOurs, docTheirs, diffTheirs, strategy) {
strategy !== null && strategy !== void 0 ? strategy : (strategy = const_1.DEFAULT_CONFLICT_RESOLUTION_STRATEGY);
if (docTheirs === undefined || diffTheirs === undefined) {
return ot_json1_1.type.apply(docOurs, this.fromDiff(diffOurs));
}
// console.log(JSON.stringify(diffOurs));
// console.log(JSON.stringify(diffTheirs));
const opOurs = this.fromDiff(diffOurs);
// console.log('opOurs: ' + JSON.stringify(opOurs) + '\n');
const opTheirs = this.fromDiff(diffTheirs);
// console.log('opTheirs: ' + JSON.stringify(opTheirs) + '\n');
const transformedOp = this.transform(opTheirs, opOurs, strategy);
// console.log('# transformed: ' + JSON.stringify(transformedOp) + '\n');
let newDoc;
if (strategy.startsWith('ours')) {
newDoc = ot_json1_1.type.apply(docOurs, transformedOp);
}
else {
// console.log('# apply to: ' + JSON.stringify(docTheirs));
newDoc = ot_json1_1.type.apply(docTheirs, transformedOp);
}
if (this._keyOfUniqueArray !== undefined && this._keyOfUniqueArray.length > 0) {
const ourTrees = [docOurs];
const theirTrees = [docTheirs];
const trees = [newDoc];
while (trees.length > 0) {
const currentTree = trees.pop();
const currentOurTree = ourTrees.pop();
const currentTheirTree = theirTrees.pop();
if (currentTree === undefined ||
currentOurTree === undefined ||
currentTheirTree === undefined)
break;
Object.keys(currentTree).forEach(key => {
if (!Array.isArray(currentTree[key]) && typeof currentTree[key] === 'object') {
trees.push(currentTree[key]);
ourTrees.push(currentOurTree[key]);
theirTrees.push(currentTheirTree[key]);
}
else if (this._keyOfUniqueArray.includes(key)) {
const array = currentTree[key];
const ourArray = currentOurTree[key];
const theirArray = currentTheirTree[key];
if (Array.isArray(array)) {
// eslint-disable-next-line complexity
const unique = array.filter((x, i, self) => {
if (self.indexOf(x) === i && i !== self.lastIndexOf(x)) {
if (strategy.startsWith('ours') &&
ourArray.indexOf(x) <= theirArray.indexOf(x)) {
return true;
}
else if (strategy.startsWith('theirs') &&
ourArray.indexOf(x) > theirArray.indexOf(x)) {
return true;
}
return false;
}
else if (self.indexOf(x) !== i && i === self.lastIndexOf(x)) {
if (strategy.startsWith('ours') &&
ourArray.indexOf(x) > theirArray.indexOf(x)) {
return true;
}
else if (strategy.startsWith('theirs') &&
ourArray.indexOf(x) <= theirArray.indexOf(x)) {
return true;
}
return false;
}
return true;
});
currentTree[key] = unique;
}
}
});
}
}
return newDoc;
}
// eslint-disable-next-line complexity
resolveConflict(opOurs, opTheirs, strategy) {
let transformedOp;
try {
// console.log('trying ours: ' + JSON.stringify(opOurs));
// console.log('trying theirs: ' + JSON.stringify(opTheirs));
if (strategy.startsWith('ours')) {
transformedOp = ot_json1_1.type.transform(opTheirs, opOurs, 'right');
}
else {
transformedOp = ot_json1_1.type.transform(opOurs, opTheirs, 'right');
}
}
catch (err) {
// console.log('conflict: ' + JSON.stringify(err));
if (err.conflict) {
// console.log('conflict: ' + JSON.stringify((err as { conflict: any }).conflict));
const conflict = err.conflict;
let conflictedOperation;
// Remove conflicted op from targetOperations
let targetOperations;
if (strategy.startsWith('ours')) {
// NOTE: op1 is opTheirs, op2 is opOurs
conflictedOperation = conflict.op1;
targetOperations = JSON.parse(JSON.stringify(opTheirs));
}
else {
conflictedOperation = conflict.op2;
targetOperations = JSON.parse(JSON.stringify(opOurs));
}
// Get JSON array of conflicted path
const conflictedPath = JSON.stringify(conflictedOperation.slice(0, -1));
// Get p, r, d, i, e
const conflictedCommands = Object.keys(conflictedOperation[conflictedOperation.length - 1]);
const resolvedOperations = [];
const stack = [
{
pathFromRoot: [],
opArray: targetOperations,
},
];
while (stack.length > 0) {
const { pathFromRoot, opArray } = stack.pop();
if (opArray.length === 0)
continue;
const opPath = [];
for (const opElm of opArray) {
if (typeof opElm === 'string') {
pathFromRoot.push(opElm);
}
else if (Array.isArray(opElm)) {
stack.push({
pathFromRoot: [...pathFromRoot],
opArray: JSON.parse(JSON.stringify(opElm)),
});
}
else {
// Operation (e.g. {p: 0})
if (JSON.stringify(pathFromRoot) === conflictedPath) {
conflictedCommands.forEach(command => delete opElm[command]);
}
if (Object.keys(opElm).length > 0) {
const resolvedOp = pathFromRoot.concat(opElm);
resolvedOperations.push(resolvedOp);
}
}
}
}
// console.log('# resolved: ' + JSON.stringify(resolvedOperations));
const resolvedOperationsComposed = resolvedOperations.reduce(ot_json1_1.type.compose, null);
// console.log('# resolved composed: ' + JSON.stringify(resolvedOperationsComposed));
if (strategy.startsWith('ours')) {
return [
JSON.parse(JSON.stringify(opOurs)),
resolvedOperationsComposed,
undefined,
];
}
return [
resolvedOperationsComposed,
JSON.parse(JSON.stringify(opTheirs)),
undefined,
];
}
throw err;
}
return [
JSON.parse(JSON.stringify(opOurs)),
JSON.parse(JSON.stringify(opTheirs)),
transformedOp,
];
}
transform(opTheirs, opOurs, strategy) {
let transformedOp;
while (transformedOp === undefined) {
[opOurs, opTheirs, transformedOp] = this.resolveConflict(opOurs, opTheirs, strategy);
}
return transformedOp;
}
}
exports.JsonPatchOT = JsonPatchOT;
//# sourceMappingURL=json_patch_ot.js.map