UNPKG

fast-json-schema-patch

Version:

Ultra-fast, schema-aware JSON patch generation and human-readable diffing. A high-performance JSON patch library that leverages schema knowledge to generate efficient, semantic patches with tools for creating human-readable diffs suitable for frontend app

1,636 lines (1,625 loc) 56.5 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion //#region node_modules/json-source-map/index.js var require_json_source_map = __commonJS({ "node_modules/json-source-map/index.js"(exports) { var escapedChars = { "b": "\b", "f": "\f", "n": "\n", "r": "\r", "t": " ", "\"": "\"", "/": "/", "\\": "\\" }; var A_CODE = "a".charCodeAt(); exports.parse = function(source, _, options) { var pointers = {}; var line = 0; var column = 0; var pos = 0; var bigint = options && options.bigint && typeof BigInt != "undefined"; return { data: _parse("", true), pointers }; function _parse(ptr, topLevel) { whitespace(); var data; map(ptr, "value"); var char = getChar(); switch (char) { case "t": read("rue"); data = true; break; case "f": read("alse"); data = false; break; case "n": read("ull"); data = null; break; case "\"": data = parseString(); break; case "[": data = parseArray(ptr); break; case "{": data = parseObject(ptr); break; default: backChar(); if ("-0123456789".indexOf(char) >= 0) data = parseNumber(); else unexpectedToken(); } map(ptr, "valueEnd"); whitespace(); if (topLevel && pos < source.length) unexpectedToken(); return data; } function whitespace() { loop: while (pos < source.length) { switch (source[pos]) { case " ": column++; break; case " ": column += 4; break; case "\r": column = 0; break; case "\n": column = 0; line++; break; default: break loop; } pos++; } } function parseString() { var str = ""; var char; while (true) { char = getChar(); if (char == "\"") break; else if (char == "\\") { char = getChar(); if (char in escapedChars) str += escapedChars[char]; else if (char == "u") str += getCharCode(); else wasUnexpectedToken(); } else str += char; } return str; } function parseNumber() { var numStr = ""; var integer = true; if (source[pos] == "-") numStr += getChar(); numStr += source[pos] == "0" ? getChar() : getDigits(); if (source[pos] == ".") { numStr += getChar() + getDigits(); integer = false; } if (source[pos] == "e" || source[pos] == "E") { numStr += getChar(); if (source[pos] == "+" || source[pos] == "-") numStr += getChar(); numStr += getDigits(); integer = false; } var result = +numStr; return bigint && integer && (result > Number.MAX_SAFE_INTEGER || result < Number.MIN_SAFE_INTEGER) ? BigInt(numStr) : result; } function parseArray(ptr) { whitespace(); var arr = []; var i = 0; if (getChar() == "]") return arr; backChar(); while (true) { var itemPtr = ptr + "/" + i; arr.push(_parse(itemPtr)); whitespace(); var char = getChar(); if (char == "]") break; if (char != ",") wasUnexpectedToken(); whitespace(); i++; } return arr; } function parseObject(ptr) { whitespace(); var obj = {}; if (getChar() == "}") return obj; backChar(); while (true) { var loc = getLoc(); if (getChar() != "\"") wasUnexpectedToken(); var key = parseString(); var propPtr = ptr + "/" + escapeJsonPointer(key); mapLoc(propPtr, "key", loc); map(propPtr, "keyEnd"); whitespace(); if (getChar() != ":") wasUnexpectedToken(); whitespace(); obj[key] = _parse(propPtr); whitespace(); var char = getChar(); if (char == "}") break; if (char != ",") wasUnexpectedToken(); whitespace(); } return obj; } function read(str) { for (var i = 0; i < str.length; i++) if (getChar() !== str[i]) wasUnexpectedToken(); } function getChar() { checkUnexpectedEnd(); var char = source[pos]; pos++; column++; return char; } function backChar() { pos--; column--; } function getCharCode() { var count = 4; var code = 0; while (count--) { code <<= 4; var char = getChar().toLowerCase(); if (char >= "a" && char <= "f") code += char.charCodeAt() - A_CODE + 10; else if (char >= "0" && char <= "9") code += +char; else wasUnexpectedToken(); } return String.fromCharCode(code); } function getDigits() { var digits = ""; while (source[pos] >= "0" && source[pos] <= "9") digits += getChar(); if (digits.length) return digits; checkUnexpectedEnd(); unexpectedToken(); } function map(ptr, prop) { mapLoc(ptr, prop, getLoc()); } function mapLoc(ptr, prop, loc) { pointers[ptr] = pointers[ptr] || {}; pointers[ptr][prop] = loc; } function getLoc() { return { line, column, pos }; } function unexpectedToken() { throw new SyntaxError("Unexpected token " + source[pos] + " in JSON at position " + pos); } function wasUnexpectedToken() { backChar(); unexpectedToken(); } function checkUnexpectedEnd() { if (pos >= source.length) throw new SyntaxError("Unexpected end of JSON input"); } }; exports.stringify = function(data, _, options) { if (!validType(data)) return; var wsLine = 0; var wsPos, wsColumn; var whitespace = typeof options == "object" ? options.space : options; switch (typeof whitespace) { case "number": var len = whitespace > 10 ? 10 : whitespace < 0 ? 0 : Math.floor(whitespace); whitespace = len && repeat(len, " "); wsPos = len; wsColumn = len; break; case "string": whitespace = whitespace.slice(0, 10); wsPos = 0; wsColumn = 0; for (var j = 0; j < whitespace.length; j++) { var char = whitespace[j]; switch (char) { case " ": wsColumn++; break; case " ": wsColumn += 4; break; case "\r": wsColumn = 0; break; case "\n": wsColumn = 0; wsLine++; break; default: throw new Error("whitespace characters not allowed in JSON"); } wsPos++; } break; default: whitespace = void 0; } var json = ""; var pointers = {}; var line = 0; var column = 0; var pos = 0; var es6 = options && options.es6 && typeof Map == "function"; _stringify(data, 0, ""); return { json, pointers }; function _stringify(_data, lvl, ptr) { map(ptr, "value"); switch (typeof _data) { case "number": case "bigint": case "boolean": out("" + _data); break; case "string": out(quoted(_data)); break; case "object": if (_data === null) out("null"); else if (typeof _data.toJSON == "function") out(quoted(_data.toJSON())); else if (Array.isArray(_data)) stringifyArray(); else if (es6) if (_data.constructor.BYTES_PER_ELEMENT) stringifyArray(); else if (_data instanceof Map) stringifyMapSet(); else if (_data instanceof Set) stringifyMapSet(true); else stringifyObject(); else stringifyObject(); } map(ptr, "valueEnd"); function stringifyArray() { if (_data.length) { out("["); var itemLvl = lvl + 1; for (var i = 0; i < _data.length; i++) { if (i) out(","); indent(itemLvl); var item = validType(_data[i]) ? _data[i] : null; var itemPtr = ptr + "/" + i; _stringify(item, itemLvl, itemPtr); } indent(lvl); out("]"); } else out("[]"); } function stringifyObject() { var keys = Object.keys(_data); if (keys.length) { out("{"); var propLvl = lvl + 1; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var value = _data[key]; if (validType(value)) { if (i) out(","); var propPtr = ptr + "/" + escapeJsonPointer(key); indent(propLvl); map(propPtr, "key"); out(quoted(key)); map(propPtr, "keyEnd"); out(":"); if (whitespace) out(" "); _stringify(value, propLvl, propPtr); } } indent(lvl); out("}"); } else out("{}"); } function stringifyMapSet(isSet) { if (_data.size) { out("{"); var propLvl = lvl + 1; var first = true; var entries = _data.entries(); var entry = entries.next(); while (!entry.done) { var item = entry.value; var key = item[0]; var value = isSet ? true : item[1]; if (validType(value)) { if (!first) out(","); first = false; var propPtr = ptr + "/" + escapeJsonPointer(key); indent(propLvl); map(propPtr, "key"); out(quoted(key)); map(propPtr, "keyEnd"); out(":"); if (whitespace) out(" "); _stringify(value, propLvl, propPtr); } entry = entries.next(); } indent(lvl); out("}"); } else out("{}"); } } function out(str) { column += str.length; pos += str.length; json += str; } function indent(lvl) { if (whitespace) { json += "\n" + repeat(lvl, whitespace); line++; column = 0; while (lvl--) { if (wsLine) { line += wsLine; column = wsColumn; } else column += wsColumn; pos += wsPos; } pos += 1; } } function map(ptr, prop) { pointers[ptr] = pointers[ptr] || {}; pointers[ptr][prop] = { line, column, pos }; } function repeat(n, str) { return Array(n + 1).join(str); } }; var VALID_TYPES = [ "number", "bigint", "boolean", "string", "object" ]; function validType(data) { return VALID_TYPES.indexOf(typeof data) >= 0; } var ESC_QUOTE = /"|\\/g; var ESC_B = /[\b]/g; var ESC_F = /\f/g; var ESC_N = /\n/g; var ESC_R = /\r/g; var ESC_T = /\t/g; function quoted(str) { str = str.replace(ESC_QUOTE, "\\$&").replace(ESC_F, "\\f").replace(ESC_B, "\\b").replace(ESC_N, "\\n").replace(ESC_R, "\\r").replace(ESC_T, "\\t"); return "\"" + str + "\""; } var ESC_0 = /~/g; var ESC_1 = /\//g; function escapeJsonPointer(str) { return str.replace(ESC_0, "~0").replace(ESC_1, "~1"); } } }); //#endregion //#region src/performance/cache.ts var import_json_source_map = __toESM(require_json_source_map(), 1); /** * Cache for JSON.stringify results * Using WeakMap with object identity as keys to avoid memory leaks */ const jsonStringCache = new WeakMap(); /** * Cache for buildPathMap results * Using WeakMap with object identity as keys to avoid memory leaks */ const pathMapCache = new WeakMap(); /** * Cache for DiffFormatter instances * Using a composite key approach for (original, new) pairs */ const formatterCache = new WeakMap(); /** * Cached version of JSON.stringify with 2-space indentation */ function cachedJsonStringify(obj) { if (typeof obj !== "object" || obj === null) return JSON.stringify(obj, null, 2); if (jsonStringCache.has(obj)) return jsonStringCache.get(obj); const result = JSON.stringify(obj, null, 2); jsonStringCache.set(obj, result); return result; } function cachedBuildPathMap(obj) { if (typeof obj !== "object" || obj === null) return {}; if (pathMapCache.has(obj)) return pathMapCache.get(obj); const jsonText = cachedJsonStringify(obj); let pathMap; try { const { pointers } = (0, import_json_source_map.parse)(jsonText); pathMap = pointers; } catch (error) { console.error("Error building path map:", error); pathMap = {}; } pathMapCache.set(obj, pathMap); return pathMap; } function getCachedFormatter(originalObj, newObj, createFormatter) { if (typeof originalObj !== "object" || originalObj === null || typeof newObj !== "object" || newObj === null) return createFormatter(originalObj, newObj); let innerCache = formatterCache.get(originalObj); if (!innerCache) { innerCache = new WeakMap(); formatterCache.set(originalObj, innerCache); } if (innerCache.has(newObj)) return innerCache.get(newObj); const formatter = createFormatter(originalObj, newObj); innerCache.set(newObj, formatter); return formatter; } //#endregion //#region src/performance/fashHash.ts /** * A simple, non-cryptographic FNV-1a hash function. * * @param {string} str The string to hash. * @returns {string} A 32-bit hash as a hex string. */ function fnv1aHash(str) { let hash = 2166136261; for (let i = 0; i < str.length; i++) { hash ^= str.charCodeAt(i); hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); } return (hash >>> 0).toString(16); } function fastHash(obj, fields) { if (fields.length === 0) return ""; let combined = ""; for (let i = 0; i < fields.length; i++) { const key = fields[i]; if (!key) continue; const value = obj[key]; if (value !== void 0) { const str = typeof value === "string" ? value : cachedJsonStringify(value); combined += `${i}:${key}=${str}|`; } } return combined ? fnv1aHash(combined) : ""; } //#endregion //#region src/performance/getEffectiveHashFields.ts /** * Utility to create hash fields from a plan or infer them from objects */ function getEffectiveHashFields(plan, obj1, obj2, fallbackFields = []) { if (plan?.hashFields && plan.hashFields.length > 0) return plan.hashFields; if (plan?.primaryKey) return [plan.primaryKey]; if (fallbackFields.length > 0) return fallbackFields; if (obj1 && obj2) { const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); const commonKeys = keys1.filter((k) => keys2.includes(k)); const idFields = commonKeys.filter((k) => k.includes("id") || k.includes("key") || k.includes("name")); return idFields.length > 0 ? idFields.slice(0, 3) : commonKeys.slice(0, 3); } return []; } //#endregion //#region src/performance/deepEqual.ts function getPlanFingerprint(plan) { if (!plan) return "default"; return `${plan.primaryKey || ""}-${plan.hashFields?.join(",") || ""}-${plan.strategy || ""}`; } function deepEqual(obj1, obj2) { if (obj1 === obj2) return true; if (obj1 && obj2 && typeof obj1 === "object" && typeof obj2 === "object") { const arrA = Array.isArray(obj1); const arrB = Array.isArray(obj2); let i; let length; if (arrA && arrB) { const arr1 = obj1; const arr2 = obj2; length = arr1.length; if (length !== arr2.length) return false; for (i = length; i-- !== 0;) if (!deepEqual(arr1[i], arr2[i])) return false; return true; } if (arrA !== arrB) return false; const keys = Object.keys(obj1); length = keys.length; if (length !== Object.keys(obj2).length) return false; for (i = length; i-- !== 0;) { const currentKey = keys[i]; if (currentKey !== void 0 && !Object.hasOwn(obj2, currentKey)) return false; if (!deepEqual(obj1[currentKey], obj2[currentKey])) return false; } return true; } return Number.isNaN(obj1) && Number.isNaN(obj2); } const eqCache = new WeakMap(); const schemaEqCache = new WeakMap(); function deepEqualMemo(obj1, obj2, hotFields = []) { if (obj1 === obj2) return true; if (obj1 == null || obj2 == null) return obj1 === obj2; const type1 = typeof obj1; const type2 = typeof obj2; if (type1 !== type2) return false; if (type1 !== "object") return obj1 === obj2; const a = obj1; const b = obj2; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length === 0 && keysB.length === 0) return true; if (keysA.length !== keysB.length) return false; const shouldHash = hotFields.length > 0 && !Array.isArray(a) && !Array.isArray(b) && keysA.length > 3; if (shouldHash) { const keyCount = keysA.length + keysB.length; const estimatedSize = keyCount * 24; if (estimatedSize >= 64 && keyCount > hotFields.length * 2) { const h1 = fastHash(a, hotFields); const h2 = fastHash(b, hotFields); if (h1 !== h2) return false; } } let inner = eqCache.get(a); if (inner?.has(b)) return inner.get(b) ?? false; const result = deepEqual(a, b); if (!inner) { inner = new WeakMap(); eqCache.set(a, inner); } inner.set(b, result); return result; } /** * Schema-aware deep equality that prioritizes comparison of significant fields first * Uses plan information to optimize equality checks */ function deepEqualSchemaAware(obj1, obj2, plan, hotFields) { if (obj1 === obj2) return true; if (obj1 == null || obj2 == null) return obj1 === obj2; const type1 = typeof obj1; const type2 = typeof obj2; if (type1 !== type2) return false; if (type1 !== "object") return obj1 === obj2; const a = obj1; const b = obj2; const effectiveHashFields = getEffectiveHashFields(plan, obj1, obj2, hotFields); if (effectiveHashFields.length > 0 && !Array.isArray(a) && !Array.isArray(b)) { const keyCount = Object.keys(a).length + Object.keys(b).length; const estimatedSize = keyCount * 24; if (estimatedSize >= 64) { const h1 = fastHash(a, effectiveHashFields); const h2 = fastHash(b, effectiveHashFields); if (h1 !== h2) return false; } } const planFingerprint = getPlanFingerprint(plan); let planCache = schemaEqCache.get(a); if (planCache?.has(b)) { const cached = planCache.get(b)?.get(planFingerprint); if (cached !== void 0) return cached; } if (plan?.requiredFields && plan.requiredFields.size > 0) { for (const field of plan.requiredFields) if (!deepEqual(a[field], b[field])) { if (!planCache) { planCache = new WeakMap(); schemaEqCache.set(a, planCache); } if (!planCache.has(b)) planCache.set(b, new Map()); planCache.get(b)?.set(planFingerprint, false); return false; } } if (plan?.primaryKey && plan.primaryKey in a && plan.primaryKey in b) { const primaryKey = plan.primaryKey; const keyEqual = deepEqual(a[primaryKey], b[primaryKey]); if (!keyEqual) { if (!planCache) { planCache = new WeakMap(); schemaEqCache.set(a, planCache); } if (!planCache.has(b)) planCache.set(b, new Map()); planCache.get(b)?.set(planFingerprint, false); return false; } } const result = deepEqual(a, b); if (!planCache) { planCache = new WeakMap(); schemaEqCache.set(a, planCache); } if (!planCache.has(b)) planCache.set(b, new Map()); planCache.get(b)?.set(planFingerprint, result); return result; } //#endregion //#region src/core/arrayDiffAlgorithms.ts function diffArrayByPrimaryKey(arr1, arr2, primaryKey, path, patches, onModification, hashFields, plan) { const effectiveHashFields = getEffectiveHashFields(plan, void 0, void 0, hashFields || []); const hashFieldsLength = effectiveHashFields.length; const hasHashFields = hashFieldsLength > 0; const arr1Length = arr1.length; const arr2Length = arr2.length; const keyToIndex = new Map(); const itemsByIndex = new Array(arr1Length); const pathPrefix = path + "/"; for (let i = 0; i < arr1Length; i++) { const item = arr1[i]; if (typeof item === "object" && item !== null) { const keyValue = item[primaryKey]; if (keyValue !== void 0 && keyValue !== null) { const keyType = typeof keyValue; if (keyType === "string" || keyType === "number") { keyToIndex.set(keyValue, i); itemsByIndex[i] = item; } } } } const modificationPatches = []; const additionPatches = []; for (let i = 0; i < arr2Length; i++) { const newItem = arr2[i]; if (typeof newItem !== "object" || newItem === null) continue; const keyValue = newItem[primaryKey]; if (keyValue === void 0) continue; const keyType = typeof keyValue; if (keyType !== "string" && keyType !== "number") continue; const oldIndex = keyToIndex.get(keyValue); if (oldIndex !== void 0) { keyToIndex.delete(keyValue); const oldItem = itemsByIndex[oldIndex]; let needsDiff = false; if (hasHashFields) { const oldItemObj = oldItem; const newItemObj = newItem; for (let j = 0; j < hashFieldsLength; j++) { const field = effectiveHashFields[j]; if (field && oldItemObj[field] !== newItemObj[field]) { needsDiff = true; break; } } if (!needsDiff && oldItem !== newItem) needsDiff = !deepEqual(oldItem, newItem); } else if (plan) needsDiff = !deepEqualSchemaAware(oldItem, newItem, plan, effectiveHashFields); else needsDiff = oldItem !== newItem && !deepEqual(oldItem, newItem); if (needsDiff) { const itemPath = pathPrefix + oldIndex; onModification(oldItem, newItem, itemPath, modificationPatches, true); } } else additionPatches.push({ op: "add", path: pathPrefix + "-", value: newItem }); } const removalIndices = Array.from(keyToIndex.values()); removalIndices.sort((a, b) => b - a); const removalPatches = new Array(removalIndices.length); for (let i = 0; i < removalIndices.length; i++) { const index = removalIndices[i]; removalPatches[i] = { op: "remove", path: pathPrefix + index, oldValue: itemsByIndex[index] }; } const totalPatches = modificationPatches.length + removalPatches.length + additionPatches.length; if (totalPatches > 0) patches.push(...modificationPatches, ...removalPatches, ...additionPatches); } function diffArrayLCS(arr1, arr2, path, patches, onModification, hashFields, plan) { const effectiveHashFields = getEffectiveHashFields(plan, void 0, void 0, hashFields || []); const n = arr1.length; const m = arr2.length; if (n === 0) { const prefixPath$1 = path === "" ? "/" : path + "/"; for (let i = 0; i < m; i++) patches.push({ op: "add", path: prefixPath$1 + i, value: arr2[i] }); return; } if (m === 0) { for (let i = n - 1; i >= 0; i--) patches.push({ op: "remove", path: path === "" ? "/" : path + "/" + i }); return; } const max = n + m; const offset = max; const bufSize = 2 * max + 1; const buffer1 = new Int32Array(bufSize); const buffer2 = new Int32Array(bufSize); buffer1.fill(-1); buffer2.fill(-1); let vPrev = buffer1; let vCurr = buffer2; vPrev[offset + 1] = 0; const trace = new Array(max + 1); let traceLen = 0; let endD = -1; const equalCache = new Map(); const cacheKey = (x$1, y$1) => x$1 << 16 | y$1; const equalAt = (x$1, y$1) => { const key = cacheKey(x$1, y$1); let result = equalCache.get(key); if (result !== void 0) return result; result = plan ? deepEqualSchemaAware(arr1[x$1], arr2[y$1], plan, effectiveHashFields) : deepEqualMemo(arr1[x$1], arr2[y$1], effectiveHashFields); equalCache.set(key, result); return result; }; const prefixPath = path === "" ? "/" : path + "/"; outer: for (let d = 0; d <= max; d++) { const traceCopy = new Int32Array(bufSize); traceCopy.set(vPrev); trace[traceLen++] = traceCopy; const dMin = -d; const dMax = d; for (let k = dMin; k <= dMax; k += 2) { const kOffset = k + offset; const vLeft = kOffset > 0 ? vPrev[kOffset - 1] : -1; const vRight = kOffset < bufSize - 1 ? vPrev[kOffset + 1] : -1; const down = k === dMin || k !== dMax && vLeft < vRight; let x$1 = down ? vRight : vLeft + 1; let y$1 = x$1 - k; while (x$1 < n && y$1 < m && equalAt(x$1, y$1)) { x$1++; y$1++; } vCurr[kOffset] = x$1; if (x$1 >= n && y$1 >= m) { const finalCopy = new Int32Array(bufSize); finalCopy.set(vCurr); trace[traceLen++] = finalCopy; endD = d; break outer; } } const tmp = vPrev; vPrev = vCurr; vCurr = tmp; vCurr.fill(-1); } if (endD === -1) return; const editScript = []; let x = n; let y = m; for (let d = endD; d > 0; d--) { const vRow = trace[d]; const k = x - y; const kOffset = k + offset; const vLeft = kOffset > 0 ? vRow[kOffset - 1] : -1; const vRight = kOffset < bufSize - 1 ? vRow[kOffset + 1] : -1; const down = k === -d || k !== d && vLeft < vRight; const prevK = down ? k + 1 : k - 1; const prevX = vRow[prevK + offset]; const prevY = prevX - prevK; while (x > prevX && y > prevY) { x--; y--; editScript.push({ op: "common", ai: x, bi: y }); } if (down) { y--; editScript.push({ op: "add", bi: y }); } else { x--; editScript.push({ op: "remove", ai: x }); } } while (x > 0 && y > 0) { x--; y--; editScript.push({ op: "common", ai: x, bi: y }); } editScript.reverse(); const optimizedScript = []; for (let i = 0; i < editScript.length; i++) { const current = editScript[i]; const next = editScript[i + 1]; if (current && current.op === "remove" && next && next.op === "add" && current.ai !== void 0 && next.bi !== void 0) { optimizedScript.push({ op: "replace", ai: current.ai, bi: next.bi }); i++; } else if (current) optimizedScript.push(current); } let currentIndex = 0; for (const operation of optimizedScript) switch (operation.op) { case "common": { const v1 = arr1[operation.ai]; const v2 = arr2[operation.bi]; if (typeof v1 === "object" && v1 !== null && typeof v2 === "object" && v2 !== null) onModification(v1, v2, prefixPath + currentIndex, patches, false); currentIndex++; break; } case "replace": { patches.push({ op: "replace", path: prefixPath + currentIndex, value: arr2[operation.bi], oldValue: arr1[operation.ai] }); currentIndex++; break; } case "remove": { patches.push({ op: "remove", path: prefixPath + currentIndex, oldValue: arr1[operation.ai] }); break; } case "add": { patches.push({ op: "add", path: prefixPath + currentIndex, value: arr2[operation.bi] }); currentIndex++; break; } } } function diffArrayUnique(arr1, arr2, path, patches) { const n = arr1.length; const m = arr2.length; const pathPrefix = path + "/"; const patches_temp = []; if (n === 0 && m === 0) return; if (n === 0) { for (let i = 0; i < m; i++) patches_temp.push({ op: "add", path: pathPrefix + "-", value: arr2[i] }); patches.push(...patches_temp); return; } if (m === 0) { for (let i = n - 1; i >= 0; i--) patches_temp.push({ op: "remove", path: pathPrefix + i, oldValue: arr1[i] }); patches.push(...patches_temp); return; } const arr1Map = new Map(); const arr2Map = new Map(); for (let i = 0; i < n; i++) arr1Map.set(arr1[i], i); for (let i = 0; i < m; i++) arr2Map.set(arr2[i], i); const minLength = Math.min(n, m); const replacedItems = new Set(); for (let i = 0; i < minLength; i++) { const val1 = arr1[i]; const val2 = arr2[i]; if (val1 !== val2) { patches_temp.push({ op: "replace", path: pathPrefix + i, value: val2, oldValue: val1 }); replacedItems.add(val2); } } const removalIndices = []; for (let i = n - 1; i >= 0; i--) { const item = arr1[i]; if (i < minLength && arr1[i] !== arr2[i]) continue; if (!arr2Map.has(item)) removalIndices.push(i); } for (const index of removalIndices) patches_temp.push({ op: "remove", path: pathPrefix + index, oldValue: arr1[index] }); for (let i = 0; i < m; i++) { const item = arr2[i]; if (i < minLength && arr1[i] !== arr2[i]) continue; if (!arr1Map.has(item)) patches_temp.push({ op: "add", path: pathPrefix + "-", value: item }); } patches.push(...patches_temp); } function checkArraysUnique(arr1, arr2) { const len1 = arr1.length; const len2 = arr2.length; if (len1 !== len2) return false; const seen1 = new Set(); const seen2 = new Set(); for (let i = 0; i < len1; i++) { const val1 = arr1[i]; const val2 = arr2[i]; if (seen1.has(val1) || seen2.has(val2)) return false; seen1.add(val1); seen2.add(val2); } return true; } //#endregion //#region src/utils/pathUtils.ts /** * Cache for path resolution results to avoid repeated computations */ const pathResolutionCache = new Map(); /** * Cache for normalized paths to avoid repeated regex operations */ const normalizedPathCache = new Map(); /** * Resolves a JSON Pointer path to get a value from an object * Handles JSON Pointer escaping (~0 for ~, ~1 for /) */ function getValueByPath(obj, path) { if (path === "") return obj; let objCache = pathResolutionCache.get(path); if (!objCache) { objCache = new WeakMap(); pathResolutionCache.set(path, objCache); } if (typeof obj === "object" && obj !== null && objCache.has(obj)) return objCache.get(obj); const parts = path.split("/").slice(1); let current = obj; for (const part of parts) { if (typeof current !== "object" || current === null) { if (typeof obj === "object" && obj !== null) objCache.set(obj, void 0); return void 0; } const key = unescapeJsonPointer(part); if (Array.isArray(current)) { const index = Number.parseInt(key, 10); if (Number.isNaN(index) || index < 0 || index >= current.length) { if (typeof obj === "object" && obj !== null) objCache.set(obj, void 0); return void 0; } current = current[index]; } else { const objCurrent = current; if (!Object.hasOwn(objCurrent, key)) { if (typeof obj === "object" && obj !== null) objCache.set(obj, void 0); return void 0; } current = objCurrent[key]; } } if (typeof obj === "object" && obj !== null) objCache.set(obj, current); return current; } /** * Resolves a patch path, handling special cases like "/-" for array append operations */ function resolvePatchPath(path, jsonObj, isForNewVersion = false) { if (path.endsWith("/-")) { const parentPath = path.slice(0, -2); if (parentPath === "") { if (Array.isArray(jsonObj)) return isForNewVersion ? `/${jsonObj.length - 1}` : `/${jsonObj.length}`; return null; } const parentValue = getValueByPath(jsonObj, parentPath); if (Array.isArray(parentValue)) return isForNewVersion ? `${parentPath}/${parentValue.length - 1}` : parentPath; } return path; } /** * Normalizes a path by removing array indices (e.g., /items/0/name -> /items/name) * Optimized to avoid regex for simple cases */ function normalizePath(path) { if (normalizedPathCache.has(path)) return normalizedPathCache.get(path); if (!/\d/.test(path)) { normalizedPathCache.set(path, path); return path; } const normalized = path.replace(/\/\d+/g, ""); normalizedPathCache.set(path, normalized); return normalized; } /** * Gets the parent path and generates a wildcard version */ function getWildcardPath(path) { const normalizedPath = normalizePath(path); const lastSlash = normalizedPath.lastIndexOf("/"); if (lastSlash >= 0) return `${normalizedPath.substring(0, lastSlash)}/*`; return null; } /** * Unescapes JSON Pointer special characters * ~1 becomes /, ~0 becomes ~ */ function unescapeJsonPointer(part) { return part.replace(/~1/g, "/").replace(/~0/g, "~"); } //#endregion //#region src/core/buildPlan.ts function _resolveRef(ref, schema) { if (!ref.startsWith("#/")) { console.warn(`Unsupported reference: ${ref}`); return null; } const path = ref.substring(2).split("/"); let current = schema; for (const part of path) { if (typeof current !== "object" || current === null || !Object.hasOwn(current, part)) return null; current = current[part]; } return current; } function _traverseSchema(subSchema, docPath, plan, schema, visited = new Set(), options) { if (!subSchema || typeof subSchema !== "object" || visited.has(subSchema)) return; visited.add(subSchema); if (subSchema.$ref) { const resolved = _resolveRef(subSchema.$ref, schema); if (resolved) _traverseSchema(resolved, docPath, plan, schema, visited, options); visited.delete(subSchema); return; } for (const keyword of [ "anyOf", "oneOf", "allOf" ]) { const schemas = subSchema[keyword]; if (schemas && Array.isArray(schemas)) { const seenFingerprints = new Set(); for (const s of schemas) { const fp = stableStringify(s); if (seenFingerprints.has(fp)) continue; seenFingerprints.add(fp); _traverseSchema(s, docPath, plan, schema, visited, options); } } } if (subSchema.type === "object") { if (subSchema.properties) for (const key in subSchema.properties) _traverseSchema(subSchema.properties[key], `${docPath}/${key}`, plan, schema, visited, options); if (typeof subSchema.additionalProperties === "object" && subSchema.additionalProperties) _traverseSchema(subSchema.additionalProperties, `${docPath}/*`, plan, schema, visited, options); } if (subSchema.type === "array" && subSchema.items) { const arrayPlan = { primaryKey: null, strategy: "lcs" }; let itemsSchema = subSchema.items; if (itemsSchema.$ref) itemsSchema = _resolveRef(itemsSchema.$ref, schema) || itemsSchema; arrayPlan.itemSchema = itemsSchema; const isPrimitive = itemsSchema && (itemsSchema.type === "string" || itemsSchema.type === "number" || itemsSchema.type === "boolean"); if (isPrimitive) arrayPlan.strategy = "unique"; const customKey = options?.primaryKeyMap?.[docPath]; if (customKey) { arrayPlan.primaryKey = customKey; arrayPlan.strategy = "primaryKey"; } else if (!isPrimitive) { const findMetadata = (s) => { let currentSchema = s; if (!currentSchema || typeof currentSchema !== "object") return null; if (currentSchema.$ref) { const resolved = _resolveRef(currentSchema.$ref, schema); if (!resolved) return null; currentSchema = resolved; } if (!currentSchema || currentSchema.type !== "object" || !currentSchema.properties) return null; const props = currentSchema.properties; const required = new Set(currentSchema.required || []); const hashFields = []; for (const key of required) { const prop = props[key]; if (prop && (prop.type === "string" || prop.type === "number")) hashFields.push(key); } const potentialKeys = [ "id", "name", "port" ]; for (const key of potentialKeys) if (required.has(key)) { const prop = props[key]; if (prop && (prop.type === "string" || prop.type === "number")) return { primaryKey: key, requiredFields: required, hashFields }; } return null; }; const schemas = itemsSchema.anyOf || itemsSchema.oneOf; let metadata = null; if (schemas) for (const s of schemas) { metadata = findMetadata(s); if (metadata?.primaryKey) break; } else metadata = findMetadata(itemsSchema); if (metadata?.primaryKey) { arrayPlan.primaryKey = metadata.primaryKey; arrayPlan.requiredFields = metadata.requiredFields; arrayPlan.hashFields = metadata.hashFields; arrayPlan.strategy = "primaryKey"; } } if (options?.basePath && !docPath.startsWith(options.basePath)) {} else { const targetPath = options?.basePath ? docPath.replace(options.basePath, "") : docPath; const existingPlan = plan.get(targetPath); if (!existingPlan) plan.set(targetPath, arrayPlan); else if (isBetterPlan(arrayPlan, existingPlan)) { mergePlanMetadata(arrayPlan, existingPlan); plan.set(targetPath, arrayPlan); } else mergePlanMetadata(existingPlan, arrayPlan); } _traverseSchema(subSchema.items, docPath, plan, schema, visited, options); } visited.delete(subSchema); } function buildPlan(options) { const plan = new Map(); const { schema,...rest } = options; _traverseSchema(schema, "", plan, schema, new Set(), rest); return plan; } function stableStringify(obj) { const seen = new WeakSet(); const stringify = (value) => { if (value && typeof value === "object") { if (seen.has(value)) return void 0; seen.add(value); const keys = Object.keys(value).sort(); const result = {}; for (const k of keys) result[k] = stringify(value[k]); return result; } return value; }; return JSON.stringify(stringify(obj)); } const STRATEGY_RANK = { primaryKey: 3, unique: 2, lcs: 1 }; function isBetterPlan(candidate, current) { const rankA = STRATEGY_RANK[candidate.strategy ?? "lcs"]; const rankB = STRATEGY_RANK[current.strategy ?? "lcs"]; if (rankA !== rankB) return rankA > rankB; if (candidate.primaryKey && !current.primaryKey) return true; if (!candidate.primaryKey && current.primaryKey) return false; const lenA = candidate.hashFields?.length ?? 0; const lenB = current.hashFields?.length ?? 0; return lenA > lenB; } function mergePlanMetadata(dst, src) { if (!dst.hashFields && src.hashFields) dst.hashFields = [...src.hashFields]; if (dst.hashFields && src.hashFields) { const merged = new Set([...dst.hashFields, ...src.hashFields]); dst.hashFields = Array.from(merged); } if (!dst.requiredFields && src.requiredFields) dst.requiredFields = new Set(src.requiredFields); } //#endregion //#region src/formatting/DiffFormatter.ts const diffFormatterCache = new Map(); function getPathLineRange(pathMap, path, jsonObj, isForNewVersion = false) { const resolvedPath = resolvePatchPath(path, jsonObj, isForNewVersion); if (!resolvedPath) return null; const info = pathMap[resolvedPath]; if (info?.value && info.valueEnd) return { start: info.value.line + 1, end: info.valueEnd.line + 1 }; const pathParts = resolvedPath.split("/").filter((p) => p !== ""); for (let i = pathParts.length; i > 0; i--) { const parentPath = `/${pathParts.slice(0, i).join("/")}`; const parentInfo = pathMap[parentPath]; if (parentInfo?.value && parentInfo.valueEnd) return { start: parentInfo.value.line + 1, end: parentInfo.valueEnd.line + 1 }; } return null; } var DiffFormatter = class { originalJson; newJson; originalPathMap; newPathMap; plan; constructor(originalJson, newJson, plan) { this.originalJson = originalJson; this.newJson = newJson; this.originalPathMap = cachedBuildPathMap(originalJson); this.newPathMap = cachedBuildPathMap(newJson); this.plan = plan; } getSampleContent(json) { const str = cachedJsonStringify(json); if (str.length <= 300) return str; return str.substring(0, 100) + str.substring(str.length / 2 - 50, str.length / 2 + 50) + str.substring(str.length - 100); } format(patches) { const patchesKey = this.createPatchesKey(patches); const planKey = this.plan ? getPlanFingerprint(this.plan) : "default"; const contentHash = fastHash({ originalSize: cachedJsonStringify(this.originalJson).length, newSize: cachedJsonStringify(this.newJson).length, originalSample: this.getSampleContent(this.originalJson), newSample: this.getSampleContent(this.newJson) }, [ "originalSize", "newSize", "originalSample", "newSample" ]); const cacheKey = `${contentHash}-${patchesKey}-${planKey}`; const cached = diffFormatterCache.get(cacheKey); if (cached) return cached; const result = this.generateDiff(patches); if (diffFormatterCache.size > 1e3) { const keys = Array.from(diffFormatterCache.keys()); for (let i = 0; i < keys.length / 2; i++) diffFormatterCache.delete(keys[i]); } diffFormatterCache.set(cacheKey, result); return result; } createPatchesKey(patches) { if (patches.length === 0) return "empty"; const patchData = { count: patches.length, operations: patches.map((p) => `${p.op}:${p.path}`).join(","), sample: patches.slice(0, 3).map((p) => p.op).join("") }; return fastHash(patchData, [ "count", "operations", "sample" ]); } generateDiff(patches) { const originalAffectedLines = new Set(); const newAffectedLines = new Set(); for (const op of patches) { if (op.op === "remove" || op.op === "replace") { const range = getPathLineRange(this.originalPathMap, op.path, this.originalJson, false); if (range) for (let i = range.start; i <= range.end; i++) originalAffectedLines.add(i); } if (op.op === "add" || op.op === "replace") { const range = getPathLineRange(this.newPathMap, op.path, this.newJson, true); if (range) for (let i = range.start; i <= range.end; i++) newAffectedLines.add(i); } } const originalFormatted = cachedJsonStringify(this.originalJson); const newFormatted = cachedJsonStringify(this.newJson); const originalLines = originalFormatted.split("\n"); const newLines = newFormatted.split("\n"); const originalDiffLines = originalLines.map((line, index) => ({ lineNumber: index + 1, content: line, type: originalAffectedLines.has(index + 1) ? "removed" : "unchanged" })); const newDiffLines = newLines.map((line, index) => ({ lineNumber: index + 1, content: line, type: newAffectedLines.has(index + 1) ? "added" : "unchanged" })); const unified = this.generateUnifiedDiff(originalDiffLines, newDiffLines); return { originalLines: originalDiffLines, newLines: newDiffLines, unifiedDiffLines: unified }; } generateUnifiedDiff(originalDiffLines, newDiffLines) { const unified = []; let i = 0; let j = 0; while (i < originalDiffLines.length && j < newDiffLines.length) { const iLine = originalDiffLines[i]; const jLine = newDiffLines[j]; if (iLine?.type === "unchanged" && jLine?.type === "unchanged") { if (iLine && jLine) unified.push({ type: "unchanged", content: iLine.content, oldLineNumber: iLine.lineNumber, newLineNumber: jLine.lineNumber, key: `unchanged-${iLine.lineNumber}-${jLine.lineNumber}` }); i++; j++; } else { while (i < originalDiffLines.length && originalDiffLines[i]?.type === "removed") { const line = originalDiffLines[i]; if (line) unified.push({ type: "removed", content: line.content, oldLineNumber: line.lineNumber, key: `removed-${line.lineNumber}` }); i++; } while (j < newDiffLines.length && newDiffLines[j]?.type === "added") { const line = newDiffLines[j]; if (line) unified.push({ type: "added", content: line.content, newLineNumber: line.lineNumber, key: `added-${line.lineNumber}` }); j++; } } } while (i < originalDiffLines.length) { const line = originalDiffLines[i]; if (line) unified.push({ type: "removed", content: line.content, oldLineNumber: line.lineNumber, key: `removed-${line.lineNumber}` }); i++; } while (j < newDiffLines.length) { const line = newDiffLines[j]; if (line) unified.push({ type: "added", content: line.content, newLineNumber: line.lineNumber, key: `added-${line.lineNumber}` }); j++; } return unified; } }; //#endregion //#region src/aggregators/StructuredDiff.ts function countChangedLines(diff) { const addCount = diff.newLines.filter((line) => line.type === "added").length; const removeCount = diff.originalLines.filter((line) => line.type === "removed").length; return { addCount, removeCount }; } var StructuredDiff = class { plan; constructor(options) { this.plan = options.plan; } getIdKeyForPath(pathPrefix) { const arrayPlan = this.getArrayPlanForPath(pathPrefix, this.plan); if (arrayPlan?.primaryKey) return arrayPlan.primaryKey; return "id"; } supportsAggregation(pathPrefix) { const hasSchemaKey = this.getArrayPlanForPath(pathPrefix, this.plan)?.primaryKey; return Boolean(hasSchemaKey); } isArrayPath(pathPrefix, config) { if (this.plan) { const arrayPlan = this.getArrayPlanForPath(pathPrefix, this.plan); if (arrayPlan) return true; } const originalValue = getValueByPath(config.original, pathPrefix); const newValue = getValueByPath(config.modified, pathPrefix); return Array.isArray(originalValue) || Array.isArray(newValue); } getArrayPlanForPath(path, plan) { const normalizedPath = path.replace(/\/\d+/g, ""); const hasLeadingSlash = path.startsWith("/"); const candidatePaths = new Set([ path, normalizedPath, hasLeadingSlash ? path.substring(1) : `/${path}`, hasLeadingSlash ? normalizedPath.substring(1) : `/${normalizedPath}` ]); for (const candidate of candidatePaths) { const arrayPlan = plan.get(candidate); if (arrayPlan) return arrayPlan; } return void 0; } aggregateWithoutChildSeparation(patches, config) { const { pathPrefix } = config; const originalParent = this.getAndStripChildArray(config.original, pathPrefix); const newParent = this.getAndStripChildArray(config.modified, pathPrefix); const parentFormatter = getCachedFormatter(originalParent, newParent, (orig, newVal) => new DiffFormatter(orig, newVal)); const parentDiffLines = parentFormatter.format(patches); const parentLineCounts = countChangedLines(parentDiffLines); return { parentDiff: { original: originalParent, new: newParent, patches, diffLines: parentDiffLines.unifiedDiffLines, ...parentLineCounts }, childDiffs: {} }; } compareObjects(obj1, obj2, plan) { if (obj1 === obj2) return true; if (!obj1 || !obj2) return false; if (plan) return deepEqualSchemaAware(obj1, obj2, plan); const hashFields = getEffectiveHashFields(plan, obj1, obj2); if (hashFields.length > 0) { const h1 = fastHash(obj1, hashFields); const h2 = fastHash(obj2, hashFields); if (h1 !== h2) return false; } return cachedJsonStringify(obj1) === cachedJsonStringify(obj2); } execute(config) { const { pathPrefix } = config; if (!this.isArrayPath(pathPrefix, config)) throw new Error(`Path ${pathPrefix} does not represent an array in the schema or data`); const patches = config.patches || new JsonSchemaPatcher({ plan: this.plan }).execute({ original: config.original, modified: config.modified }); if (!this.supportsAggregation(pathPrefix)) return this.aggregateWithoutChildSeparation(patches, config); const idKey = this.getIdKeyForPath(pathPrefix); const parentPatches = []; const childPatchesById = {}; const originalChildren = getValueByPath(config.original, pathPrefix) || []; const originalChildIdsByIndex = originalChildren.map((child) => child[idKey]); for (const patch of patches) { if (!patch.path.startsWith(pathPrefix)) { parentPatches.push(patch); continue; } const relativePath = patch.path.substring(pathPrefix.length); const match = relativePath.match(/^\/(\d+|-)$/); const matchIndex = match?.[1]; let childId; if (matchIndex) { if (matchIndex === "-" && patch.op === "add") childId = patch.value?.[idKey]; else if (matchIndex !== "-") { const index = Number.parseInt(matchIndex, 10); if (patch.op === "add") childId = patch.value?.[idKey]; else childId = originalChildIdsByIndex[index]; } } else { const nestedMatch = relativePath.match(/^\/(\d+)/); const nestedIndex = nestedMatch?.[1]; if (nestedIndex) { const index = Number.parseInt(nestedIndex, 10); childId = originalChildIdsByIndex[index]; } } if (childId) { if (!(childId in childPatchesById)) childPatchesById[childId] = []; childPatchesById[childId]?.push(patch); } else parentPatches.push(patch); } const parentPath = pathPrefix.substring(0, pathPrefix.lastIndexOf("/")); const originalParent = this.getAndStripChildArray(config.original, pathPrefix); const newParent = this.getAndStripChildArray(config.modified, pathPrefix); const parentFormatter = getCachedFormatter(originalParent, newParent, (orig, newVal) => new DiffFormatter(orig, newVal)); const transformedParentPatches = parentPatches.map((p) => { if (p.path.startsWith(parentPath)) return { ...p, path: p.path.substring(parentPath.length) }; return p; }); const parentDiffLines = parentFormatter.format(transformedParentPatches); const parentLineCounts = countChangedLines(parentDiffLines); const childDiffs = {}; const newChildren = getValueByPath(config.modified, pathPrefix) || []; const originalChildrenById = new Map(originalChildren.map((c) => [c[idKey], c])); const newChildrenById = new Map(newChildren.map((c) => [c[idKey], c])); const allChildIds = new Set([...originalChildrenById.keys(), ...newChildrenById.keys()]); for (const childId of allChildIds) { const originalChild = originalChildrenById.get(childId) || null; const newChild = newChildrenById.get(childId) || null; const patchesForChild = childPatchesById[childId] || []; const transformedPatches = patchesForChild.map((p) => { const originalIndex = originalChildren.findIndex((c) => c[idKey] === childId); if (originalIndex >= 0) { const childPathPrefix = `${pathPrefix}/${originalIndex}`; return { ...p, path: p.path.substring(childPathPrefix.length) }; } if (p.op === "add") return { ...p, path: "" }; const pathMatch = p.path.match(new RegExp(`^${pathPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/(\d+)`)); const pathIndex = pathMatch?.[1]; if (pathIndex) { const index = Number.parseInt(pathIndex, 10); const childPathPrefix = `${pathPrefix}/${index}`; return { ...p, path: p.path.substring(childPathPrefix.length) }; } return p; }); const formatter = getCachedFormatter(originalChild, newChild, (orig, newVal) => new DiffFormatter(orig, newVal)); let diffLines; let lineCounts; if (originalChild && !newChild) { const originalFormatted = cachedJsonStringify(originalChild); const originalLines = originalFormatted.split("\n"); diffLines = { originalLines: originalLines.map((line, index) => ({ lineNumber: index + 1, content: line, type: "removed" })), newLines: [{ lineNumber: 1, content: "null", type: "unchanged" }], unifiedDiffLines: originalLines.map((line, index) => ({ type: "removed", content: line, oldLineNumber: index + 1, key: `removed-${index + 1}` })) }; lineCounts = { addCount: 0, removeCount: originalLines.length }; } else if (!or