datum-merge
Version:
Simplified diff and merging for deeply nested objects
431 lines (430 loc) • 14.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOrderIndependentHash = exports.realTypeOf = exports.applyDiff = exports.revertChange = exports.applyChange = exports.orderIndependentDeepDiff = exports.observableDiff = exports.accumulateDiff = exports.orderIndependentDiff = exports.diff = void 0;
const typeNormalizer = {
normalize: function (currentPath, key, lhs, rhs) {
if (realTypeOf(lhs) === 'regexp' && realTypeOf(rhs) === 'regexp') {
lhs = lhs.toString();
rhs = rhs.toString();
}
if (realTypeOf(lhs) === 'date' && realTypeOf(rhs) === 'date') {
lhs = lhs.valueOf();
rhs = rhs.valueOf();
}
return [lhs, rhs];
}
};
function diff(lhs, rhs, prefilter) {
const changes = [];
deepDiff(lhs, rhs, changes, prefilter);
return (changes === null || changes === void 0 ? void 0 : changes.length) ? changes : undefined;
}
exports.diff = diff;
function orderIndependentDiff(lhs, rhs, prefilter) {
const changes = observableDiff(lhs, rhs, undefined, prefilter, true);
return (changes === null || changes === void 0 ? void 0 : changes.length) ? changes : undefined;
}
exports.orderIndependentDiff = orderIndependentDiff;
function observableDiff(lhs, rhs, observer, prefilter, orderIndependent) {
const changes = [];
deepDiff(lhs, rhs, changes, prefilter, undefined, undefined, undefined, orderIndependent);
if (observer) {
changes.forEach((c) => observer(c));
}
return changes;
}
exports.observableDiff = observableDiff;
function accumulateDiff(lhs, rhs, prefilter, accum, orderIndependent) {
const observer = (accum) ?
function (difference) {
if (difference) {
accum.push(difference);
}
} : undefined;
const changes = observableDiff(lhs, rhs, observer, prefilter, orderIndependent);
return accum ? accum : (changes.length) ? changes : undefined;
}
exports.accumulateDiff = accumulateDiff;
function orderIndependentDeepDiff(lhs, rhs, changes, prefilter, path, key, stack) {
deepDiff(lhs, rhs, changes, prefilter, path, key, stack, true);
}
exports.orderIndependentDeepDiff = orderIndependentDeepDiff;
function deepDiff(lhs, rhs, changes, prefilter, path, key, stack, orderIndependent = false) {
changes = changes || [];
path = path || [];
stack = stack || [];
const currentPath = path.slice(0);
if (typeof key !== 'undefined' && key !== null) {
if (prefilter) {
if (typeof (prefilter) === 'function' && prefilter(currentPath, key)) {
return;
}
else if (typeof (prefilter) === 'object') {
if (prefilter.prefilter && prefilter.prefilter(currentPath, key)) {
return;
}
if (prefilter.normalize) {
const alt = prefilter.normalize(currentPath, key, lhs, rhs);
if (alt) {
lhs = alt[0];
rhs = alt[1];
}
}
}
}
currentPath.push(key);
}
if (realTypeOf(lhs) === 'regexp' && realTypeOf(rhs) === 'regexp') {
lhs = lhs.toString();
rhs = rhs.toString();
}
const ltype = typeof lhs;
const rtype = typeof rhs;
const ldefined = ltype !== 'undefined' ||
(stack && (stack.length > 0) && stack[stack.length - 1].lhs &&
Object.getOwnPropertyDescriptor(stack[stack.length - 1].lhs, key));
const rdefined = rtype !== 'undefined' ||
(stack && (stack.length > 0) && stack[stack.length - 1].rhs &&
Object.getOwnPropertyDescriptor(stack[stack.length - 1].rhs, key));
if (!ldefined && rdefined) {
changes.push({
kind: 'N',
path: currentPath,
rhs
});
return;
}
else if (!rdefined && ldefined) {
changes.push({
kind: 'D',
path: currentPath,
lhs
});
return;
}
else if (realTypeOf(lhs) !== realTypeOf(rhs)) {
changes.push({
kind: 'E',
path: currentPath,
lhs,
rhs
});
return;
}
if (realTypeOf(lhs) === 'date'
&& (lhs.valueOf() - rhs.valueOf()) !== 0) {
changes.push({
kind: 'E',
path: currentPath,
lhs,
rhs
});
return;
}
if (ltype === 'object' && lhs !== null && rhs !== null) {
let other = false;
for (let i = stack.length - 1; i > -1; --i) {
if (stack[i].lhs === lhs) {
other = true;
break;
}
}
if (!other) {
stack.push({ lhs: lhs, rhs: rhs });
if (Array.isArray(lhs) && Array.isArray(rhs)) {
let lArr = lhs;
let rArr = rhs;
if (orderIndependent) {
lArr = lArr.slice(0).sort(function (a, b) {
return getOrderIndependentHash(a) - getOrderIndependentHash(b);
});
rArr = rArr.slice(0).sort(function (a, b) {
return getOrderIndependentHash(a) - getOrderIndependentHash(b);
});
}
let i = rArr.length - 1;
let j = lArr.length - 1;
while (i > j) {
changes.push({
kind: 'A',
path: currentPath,
index: i,
item: { kind: 'N', rhs: rArr[i--], path: undefined },
});
}
while (j > i) {
changes.push({
kind: 'A',
path: currentPath,
index: j,
item: { kind: 'D', lhs: lArr[j--], path: undefined },
});
}
for (; i >= 0; --i) {
deepDiff(lArr[i], rArr[i], changes, prefilter, currentPath, i, stack, orderIndependent);
}
}
else {
const lObj = lhs;
const rObj = rhs;
const akeys = [...Object.keys(lObj), ...Object.getOwnPropertySymbols(lObj)];
const pkeys = [...Object.keys(rObj), ...Object.getOwnPropertySymbols(rObj)];
for (let i = 0; i < akeys.length; ++i) {
const k = akeys[i];
const ki = pkeys.indexOf(k);
if (ki >= 0) {
deepDiff(lObj[k], rObj[k], changes, prefilter, currentPath, k, stack, orderIndependent);
pkeys[ki] = null;
}
else {
deepDiff(lObj[k], undefined, changes, prefilter, currentPath, k, stack, orderIndependent);
}
}
for (let i = 0; i < pkeys.length; ++i) {
const k = pkeys[i];
if (k) {
deepDiff(undefined, rObj[k], changes, prefilter, currentPath, k, stack, orderIndependent);
}
}
}
stack.pop();
}
else if (lhs !== rhs) {
changes.push({
kind: 'E',
path: currentPath,
lhs,
rhs
});
}
}
else if (lhs !== rhs) {
if (!(ltype === 'number' && isNaN(lhs) && isNaN(rhs))) {
changes.push({
kind: 'E',
path: currentPath,
lhs,
rhs
});
}
}
}
function applyDiff(target, source, filter) {
if (!target || !source) {
return target;
}
const onChange = function (change) {
if (!filter || filter(target, source, change)) {
applyChange(target, undefined, change);
}
};
observableDiff(target, source, onChange);
return target;
}
exports.applyDiff = applyDiff;
function applyChange(target, unused, change) {
var _a;
if (!target || !change || !change.kind) {
return;
}
let it = target;
const rootPath = !((_a = change.path) === null || _a === void 0 ? void 0 : _a.length);
const last = rootPath ? 0 : change.path.length - 1;
let i = -1;
while (++i < last) {
if (typeof it[change.path[i]] === 'undefined') {
it[change.path[i]] = (typeof change.path[i + 1] !== 'undefined'
&& typeof change.path[i + 1] === 'number') ? [] : {};
}
it = it[change.path[i]];
}
switch (change.kind) {
case 'A':
if (!rootPath && typeof it[change.path[i]] === 'undefined') {
it[change.path[i]] = [];
}
applyArrayChange(rootPath ? it : it[change.path[i]], change.index, change.item);
break;
case 'D':
delete it[change.path[i]];
break;
case 'E':
case 'N':
it[change.path[i]] = change.rhs;
break;
}
}
exports.applyChange = applyChange;
function applyArrayChange(arr, index, change) {
if (change.path && change.path.length > 0) {
const last = change.path.length - 1;
let it = arr[index];
let i;
for (i = 0; i < last; i++) {
it = it[change.path[i]];
}
switch (change.kind) {
case 'A':
applyArrayChange(it[change.path[i]], change.index, change.item);
break;
case 'D':
delete it[change.path[i]];
break;
case 'E':
case 'N':
it[change.path[i]] = change.rhs;
break;
}
}
else {
switch (change.kind) {
case 'A':
applyArrayChange(arr[index], change.index, change.item);
break;
case 'D':
arr = arrayRemove(arr, index);
break;
case 'E':
case 'N':
arr[index] = change.rhs;
break;
}
}
return arr;
}
function revertChange(target, unused, change) {
var _a;
if (!target || !change || !change.kind) {
return;
}
let it = target;
const rootPath = !((_a = change.path) === null || _a === void 0 ? void 0 : _a.length);
const last = rootPath ? 0 : change.path.length - 1;
let i;
for (i = 0; i < last; i++) {
if (typeof it[change.path[i]] === 'undefined') {
it[change.path[i]] = {};
}
it = it[change.path[i]];
}
switch (change.kind) {
case 'A':
revertArrayChange(rootPath ? it : it[change.path[i]], change.index, change.item);
break;
case 'D':
it[change.path[i]] = change.lhs;
break;
case 'E':
it[change.path[i]] = change.lhs;
break;
case 'N':
delete it[change.path[i]];
break;
}
}
exports.revertChange = revertChange;
function revertArrayChange(arr, index, change) {
if (change.path && change.path.length > 0) {
const last = change.path.length - 1;
let it = arr[index];
let i;
for (i = 0; i < last; i++) {
it = it[change.path[i]];
}
switch (change.kind) {
case 'A':
revertArrayChange(it[change.path[i]], change.index, change.item);
break;
case 'D':
it[change.path[i]] = change.lhs;
break;
case 'E':
it[change.path[i]] = change.lhs;
break;
case 'N':
delete it[change.path[i]];
break;
}
}
else {
switch (change.kind) {
case 'A':
revertArrayChange(arr[index], change.index, change.item);
break;
case 'D':
arr[index] = change.lhs;
break;
case 'E':
arr[index] = change.lhs;
break;
case 'N':
arr = arrayRemove(arr, index);
break;
}
}
return arr;
}
function arrayRemove(arr, index) {
index = index < 0 ? arr.length + index : index;
arr.splice(index, 1);
return arr;
}
function realTypeOf(val) {
const type = typeof val;
if (type !== 'object') {
return type;
}
if (val === Math) {
return 'math';
}
else if (val === null) {
return 'null';
}
else if (Array.isArray(val)) {
return 'array';
}
else if (Object.prototype.toString.call(val) === '[object Date]') {
return 'date';
}
else if (typeof val.toString === 'function' && /^\/.*\//.test(val.toString())) {
return 'regexp';
}
return 'object';
}
exports.realTypeOf = realTypeOf;
function getOrderIndependentHash(val) {
let accum = 0;
const type = realTypeOf(val);
if (type === 'array') {
val.forEach(function (item) {
accum += getOrderIndependentHash(item);
});
const arrayString = `[type: array, hash: ${accum}]`;
return accum + hashThisString(arrayString);
}
if (type === 'object') {
for (let key in val) {
if (val.hasOwnProperty(key)) {
const keyValueHash = getOrderIndependentHash(val[key]);
const keyValueString = `[ type: object, key: ${key}, value hash: ${keyValueHash}]`;
accum += hashThisString(keyValueString);
}
}
return accum;
}
const stringToHash = `[ type: ${type} ; value: ${val}]`;
return accum + hashThisString(stringToHash);
}
exports.getOrderIndependentHash = getOrderIndependentHash;
function hashThisString(str) {
let hash = 0;
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}