UNPKG

datum-merge

Version:

Simplified diff and merging for deeply nested objects

431 lines (430 loc) 14.3 kB
"use strict"; 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; }