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 lines 124 kB
{"version":3,"file":"index.cjs","names":["obj: JsonValue","pathMap: PathMap","originalObj: JsonValue","newObj: JsonValue","createFormatter: (original: JsonValue, newValue: JsonValue) => T","str: string","obj: JsonObject","fields: string[]","plan?: ArrayPlan","obj1?: JsonObject","obj2?: JsonObject","fallbackFields: string[]","plan?: ArrayPlan","obj1: unknown","obj2: unknown","i: number","length: number","hotFields: string[]","hotFields?: string[]","arr1: JsonArray","arr2: JsonArray","primaryKey: string","path: string","patches: Operation[]","onModification: ModificationCallback","hashFields?: string[]","plan?: ArrayPlan","modificationPatches: Operation[]","additionPatches: Operation[]","removalPatches: Operation[]","prefixPath","x: number","y: number","x","y","editScript: Array<{\n op: \"common\" | \"remove\" | \"add\";\n ai?: number;\n bi?: number;\n }>","optimizedScript: Array<{\n op: \"common\" | \"remove\" | \"add\" | \"replace\";\n ai?: number;\n bi?: number;\n }>","patches_temp: Operation[]","removalIndices: number[]","obj: JsonValue","path: string","current: JsonValue","jsonObj: JsonValue","part: string","ref: string","schema: Schema","current: unknown","subSchema: JSONSchema | boolean","docPath: string","plan: Plan","visited: Set<object>","options?: Omit<BuildPlanOptions, \"schema\">","arrayPlan: ArrayPlan","s: JSONSchema","hashFields: string[]","metadata: ReturnType<typeof findMetadata> | null","options: BuildPlanOptions","obj: unknown","value: unknown","result: Record<string, unknown>","STRATEGY_RANK: Record<NonNullable<ArrayPlan[\"strategy\"]>, number>","candidate: ArrayPlan","current: ArrayPlan","dst: ArrayPlan","src: ArrayPlan","pathMap: PathMap","path: string","jsonObj: JsonValue","originalJson: JsonValue","newJson: JsonValue","plan?: ArrayPlan","json: JsonValue","patches: Operation[]","originalDiffLines: DiffLine[]","newDiffLines: DiffLine[]","unified: StructuredDiffLine[]","diff: FormattedDiffLines","options: { plan: Plan }","pathPrefix: string","config: StructuredDiffConfig","path: string","plan: Plan","patches: Operation[]","obj1: JsonObject | null","obj2: JsonObject | null","plan?: ArrayPlan","parentPatches: Operation[]","childPatchesById: Record<string, Operation[]>","childId: string | undefined","childDiffs: Record<string, FormattedChildDiff>","diffLines: FormattedDiffLines","lineCounts: { addCount: number; removeCount: number }","doc: JsonValue","options: { plan: Plan }","path: string","patches: Operation[]","obj1: JsonValue | undefined","obj2: JsonValue | undefined","obj1: JsonObject","obj2: JsonObject","arr1: JsonArray","arr2: JsonArray","hashFields: string[]","oldVal: JsonValue","newVal: JsonValue","skipEqualityCheck?: boolean","path","patches","plan: ArrayPlan | undefined","skipEqualityCheck: boolean"],"sources":["../node_modules/json-source-map/index.js","../src/performance/cache.ts","../src/performance/fashHash.ts","../src/performance/getEffectiveHashFields.ts","../src/performance/deepEqual.ts","../src/core/arrayDiffAlgorithms.ts","../src/utils/pathUtils.ts","../src/core/buildPlan.ts","../src/formatting/DiffFormatter.ts","../src/aggregators/StructuredDiff.ts","../src/index.ts"],"sourcesContent":["'use strict';\n\nvar escapedChars = {\n 'b': '\\b',\n 'f': '\\f',\n 'n': '\\n',\n 'r': '\\r',\n 't': '\\t',\n '\"': '\"',\n '/': '/',\n '\\\\': '\\\\'\n};\n\nvar A_CODE = 'a'.charCodeAt();\n\n\nexports.parse = function (source, _, options) {\n var pointers = {};\n var line = 0;\n var column = 0;\n var pos = 0;\n var bigint = options && options.bigint && typeof BigInt != 'undefined';\n return {\n data: _parse('', true),\n pointers: pointers\n };\n\n function _parse(ptr, topLevel) {\n whitespace();\n var data;\n map(ptr, 'value');\n var char = getChar();\n switch (char) {\n case 't': read('rue'); data = true; break;\n case 'f': read('alse'); data = false; break;\n case 'n': read('ull'); data = null; break;\n case '\"': data = parseString(); break;\n case '[': data = parseArray(ptr); break;\n case '{': data = parseObject(ptr); break;\n default:\n backChar();\n if ('-0123456789'.indexOf(char) >= 0)\n data = parseNumber();\n else\n unexpectedToken();\n }\n map(ptr, 'valueEnd');\n whitespace();\n if (topLevel && pos < source.length) unexpectedToken();\n return data;\n }\n\n function whitespace() {\n loop:\n while (pos < source.length) {\n switch (source[pos]) {\n case ' ': column++; break;\n case '\\t': column += 4; break;\n case '\\r': column = 0; break;\n case '\\n': column = 0; line++; break;\n default: break loop;\n }\n pos++;\n }\n }\n\n function parseString() {\n var str = '';\n var char;\n while (true) {\n char = getChar();\n if (char == '\"') {\n break;\n } else if (char == '\\\\') {\n char = getChar();\n if (char in escapedChars)\n str += escapedChars[char];\n else if (char == 'u')\n str += getCharCode();\n else\n wasUnexpectedToken();\n } else {\n str += char;\n }\n }\n return str;\n }\n\n function parseNumber() {\n var numStr = '';\n var integer = true;\n if (source[pos] == '-') numStr += getChar();\n\n numStr += source[pos] == '0'\n ? getChar()\n : getDigits();\n\n if (source[pos] == '.') {\n numStr += getChar() + getDigits();\n integer = false;\n }\n\n if (source[pos] == 'e' || source[pos] == 'E') {\n numStr += getChar();\n if (source[pos] == '+' || source[pos] == '-') numStr += getChar();\n numStr += getDigits();\n integer = false;\n }\n\n var result = +numStr;\n return bigint && integer && (result > Number.MAX_SAFE_INTEGER || result < Number.MIN_SAFE_INTEGER)\n ? BigInt(numStr)\n : result;\n }\n\n function parseArray(ptr) {\n whitespace();\n var arr = [];\n var i = 0;\n if (getChar() == ']') return arr;\n backChar();\n\n while (true) {\n var itemPtr = ptr + '/' + i;\n arr.push(_parse(itemPtr));\n whitespace();\n var char = getChar();\n if (char == ']') break;\n if (char != ',') wasUnexpectedToken();\n whitespace();\n i++;\n }\n return arr;\n }\n\n function parseObject(ptr) {\n whitespace();\n var obj = {};\n if (getChar() == '}') return obj;\n backChar();\n\n while (true) {\n var loc = getLoc();\n if (getChar() != '\"') wasUnexpectedToken();\n var key = parseString();\n var propPtr = ptr + '/' + escapeJsonPointer(key);\n mapLoc(propPtr, 'key', loc);\n map(propPtr, 'keyEnd');\n whitespace();\n if (getChar() != ':') wasUnexpectedToken();\n whitespace();\n obj[key] = _parse(propPtr);\n whitespace();\n var char = getChar();\n if (char == '}') break;\n if (char != ',') wasUnexpectedToken();\n whitespace();\n }\n return obj;\n }\n\n function read(str) {\n for (var i=0; i<str.length; i++)\n if (getChar() !== str[i]) wasUnexpectedToken();\n }\n\n function getChar() {\n checkUnexpectedEnd();\n var char = source[pos];\n pos++;\n column++; // new line?\n return char;\n }\n\n function backChar() {\n pos--;\n column--;\n }\n\n function getCharCode() {\n var count = 4;\n var code = 0;\n while (count--) {\n code <<= 4;\n var char = getChar().toLowerCase();\n if (char >= 'a' && char <= 'f')\n code += char.charCodeAt() - A_CODE + 10;\n else if (char >= '0' && char <= '9')\n code += +char;\n else\n wasUnexpectedToken();\n }\n return String.fromCharCode(code);\n }\n\n function getDigits() {\n var digits = '';\n while (source[pos] >= '0' && source[pos] <= '9')\n digits += getChar();\n\n if (digits.length) return digits;\n checkUnexpectedEnd();\n unexpectedToken();\n }\n\n function map(ptr, prop) {\n mapLoc(ptr, prop, getLoc());\n }\n\n function mapLoc(ptr, prop, loc) {\n pointers[ptr] = pointers[ptr] || {};\n pointers[ptr][prop] = loc;\n }\n\n function getLoc() {\n return {\n line: line,\n column: column,\n pos: pos\n };\n }\n\n function unexpectedToken() {\n throw new SyntaxError('Unexpected token ' + source[pos] + ' in JSON at position ' + pos);\n }\n\n function wasUnexpectedToken() {\n backChar();\n unexpectedToken();\n }\n\n function checkUnexpectedEnd() {\n if (pos >= source.length)\n throw new SyntaxError('Unexpected end of JSON input');\n }\n};\n\n\nexports.stringify = function (data, _, options) {\n if (!validType(data)) return;\n var wsLine = 0;\n var wsPos, wsColumn;\n var whitespace = typeof options == 'object'\n ? options.space\n : options;\n switch (typeof whitespace) {\n case 'number':\n var len = whitespace > 10\n ? 10\n : whitespace < 0\n ? 0\n : Math.floor(whitespace);\n whitespace = len && repeat(len, ' ');\n wsPos = len;\n wsColumn = len;\n break;\n case 'string':\n whitespace = whitespace.slice(0, 10);\n wsPos = 0;\n wsColumn = 0;\n for (var j=0; j<whitespace.length; j++) {\n var char = whitespace[j];\n switch (char) {\n case ' ': wsColumn++; break;\n case '\\t': wsColumn += 4; break;\n case '\\r': wsColumn = 0; break;\n case '\\n': wsColumn = 0; wsLine++; break;\n default: throw new Error('whitespace characters not allowed in JSON');\n }\n wsPos++;\n }\n break;\n default:\n whitespace = undefined;\n }\n\n var json = '';\n var pointers = {};\n var line = 0;\n var column = 0;\n var pos = 0;\n var es6 = options && options.es6 && typeof Map == 'function';\n _stringify(data, 0, '');\n return {\n json: json,\n pointers: pointers\n };\n\n function _stringify(_data, lvl, ptr) {\n map(ptr, 'value');\n switch (typeof _data) {\n case 'number':\n case 'bigint':\n case 'boolean':\n out('' + _data); break;\n case 'string':\n out(quoted(_data)); break;\n case 'object':\n if (_data === null) {\n out('null');\n } else if (typeof _data.toJSON == 'function') {\n out(quoted(_data.toJSON()));\n } else if (Array.isArray(_data)) {\n stringifyArray();\n } else if (es6) {\n if (_data.constructor.BYTES_PER_ELEMENT)\n stringifyArray();\n else if (_data instanceof Map)\n stringifyMapSet();\n else if (_data instanceof Set)\n stringifyMapSet(true);\n else\n stringifyObject();\n } else {\n stringifyObject();\n }\n }\n map(ptr, 'valueEnd');\n\n function stringifyArray() {\n if (_data.length) {\n out('[');\n var itemLvl = lvl + 1;\n for (var i=0; i<_data.length; i++) {\n if (i) out(',');\n indent(itemLvl);\n var item = validType(_data[i]) ? _data[i] : null;\n var itemPtr = ptr + '/' + i;\n _stringify(item, itemLvl, itemPtr);\n }\n indent(lvl);\n out(']');\n } else {\n out('[]');\n }\n }\n\n function stringifyObject() {\n var keys = Object.keys(_data);\n if (keys.length) {\n out('{');\n var propLvl = lvl + 1;\n for (var i=0; i<keys.length; i++) {\n var key = keys[i];\n var value = _data[key];\n if (validType(value)) {\n if (i) out(',');\n var propPtr = ptr + '/' + escapeJsonPointer(key);\n indent(propLvl);\n map(propPtr, 'key');\n out(quoted(key));\n map(propPtr, 'keyEnd');\n out(':');\n if (whitespace) out(' ');\n _stringify(value, propLvl, propPtr);\n }\n }\n indent(lvl);\n out('}');\n } else {\n out('{}');\n }\n }\n\n function stringifyMapSet(isSet) {\n if (_data.size) {\n out('{');\n var propLvl = lvl + 1;\n var first = true;\n var entries = _data.entries();\n var entry = entries.next();\n while (!entry.done) {\n var item = entry.value;\n var key = item[0];\n var value = isSet ? true : item[1];\n if (validType(value)) {\n if (!first) out(',');\n first = false;\n var propPtr = ptr + '/' + escapeJsonPointer(key);\n indent(propLvl);\n map(propPtr, 'key');\n out(quoted(key));\n map(propPtr, 'keyEnd');\n out(':');\n if (whitespace) out(' ');\n _stringify(value, propLvl, propPtr);\n }\n entry = entries.next();\n }\n indent(lvl);\n out('}');\n } else {\n out('{}');\n }\n }\n }\n\n function out(str) {\n column += str.length;\n pos += str.length;\n json += str;\n }\n\n function indent(lvl) {\n if (whitespace) {\n json += '\\n' + repeat(lvl, whitespace);\n line++;\n column = 0;\n while (lvl--) {\n if (wsLine) {\n line += wsLine;\n column = wsColumn;\n } else {\n column += wsColumn;\n }\n pos += wsPos;\n }\n pos += 1; // \\n character\n }\n }\n\n function map(ptr, prop) {\n pointers[ptr] = pointers[ptr] || {};\n pointers[ptr][prop] = {\n line: line,\n column: column,\n pos: pos\n };\n }\n\n function repeat(n, str) {\n return Array(n + 1).join(str);\n }\n};\n\n\nvar VALID_TYPES = ['number', 'bigint', 'boolean', 'string', 'object'];\nfunction validType(data) {\n return VALID_TYPES.indexOf(typeof data) >= 0;\n}\n\n\nvar ESC_QUOTE = /\"|\\\\/g;\nvar ESC_B = /[\\b]/g;\nvar ESC_F = /\\f/g;\nvar ESC_N = /\\n/g;\nvar ESC_R = /\\r/g;\nvar ESC_T = /\\t/g;\nfunction quoted(str) {\n str = str.replace(ESC_QUOTE, '\\\\$&')\n .replace(ESC_F, '\\\\f')\n .replace(ESC_B, '\\\\b')\n .replace(ESC_N, '\\\\n')\n .replace(ESC_R, '\\\\r')\n .replace(ESC_T, '\\\\t');\n return '\"' + str + '\"';\n}\n\n\nvar ESC_0 = /~/g;\nvar ESC_1 = /\\//g;\nfunction escapeJsonPointer(str) {\n return str.replace(ESC_0, '~0')\n .replace(ESC_1, '~1');\n}\n","import {parse} from \"json-source-map\"\nimport type {JsonValue, PathMap} from \"../types\"\n\n/**\n * Cache for JSON.stringify results\n * Using WeakMap with object identity as keys to avoid memory leaks\n */\nconst jsonStringCache = new WeakMap<object, string>()\n\n/**\n * Cache for buildPathMap results\n * Using WeakMap with object identity as keys to avoid memory leaks\n */\nconst pathMapCache = new WeakMap<object, PathMap>()\n\n/**\n * Cache for DiffFormatter instances\n * Using a composite key approach for (original, new) pairs\n */\nconst formatterCache = new WeakMap<object, WeakMap<object, unknown>>()\n\n/**\n * Cached version of JSON.stringify with 2-space indentation\n */\nexport function cachedJsonStringify(obj: JsonValue): string {\n if (typeof obj !== \"object\" || obj === null) {\n return JSON.stringify(obj, null, 2)\n }\n\n if (jsonStringCache.has(obj)) {\n return jsonStringCache.get(obj) as string\n }\n\n const result = JSON.stringify(obj, null, 2)\n jsonStringCache.set(obj, result)\n return result\n}\n\nexport function cachedBuildPathMap(obj: JsonValue): PathMap {\n if (typeof obj !== \"object\" || obj === null) {\n // For primitives, just return empty path map since they don't have complex structure\n return {}\n }\n\n if (pathMapCache.has(obj)) {\n return pathMapCache.get(obj) as PathMap\n }\n\n const jsonText = cachedJsonStringify(obj)\n let pathMap: PathMap\n\n try {\n const {pointers} = parse(jsonText)\n pathMap = pointers as unknown as PathMap\n } catch (error) {\n console.error(\"Error building path map:\", error)\n pathMap = {}\n }\n\n pathMapCache.set(obj, pathMap)\n return pathMap\n}\n\nexport function getCachedFormatter<T>(\n originalObj: JsonValue,\n newObj: JsonValue,\n createFormatter: (original: JsonValue, newValue: JsonValue) => T,\n): T {\n if (\n typeof originalObj !== \"object\" ||\n originalObj === null ||\n typeof newObj !== \"object\" ||\n newObj === null\n ) {\n return createFormatter(originalObj, newObj)\n }\n\n let innerCache = formatterCache.get(originalObj)\n if (!innerCache) {\n innerCache = new WeakMap()\n formatterCache.set(originalObj, innerCache)\n }\n\n if (innerCache.has(newObj)) {\n return innerCache.get(newObj) as T\n }\n\n const formatter = createFormatter(originalObj, newObj)\n innerCache.set(newObj, formatter)\n return formatter\n}\n","import type {JsonObject} from \"../types\"\nimport { cachedJsonStringify } from \"./cache\"\n\n/**\n * A simple, non-cryptographic FNV-1a hash function.\n *\n * @param {string} str The string to hash.\n * @returns {string} A 32-bit hash as a hex string.\n */\nfunction fnv1aHash(str: string): string {\n let hash = 0x811c9dc5 // FNV_offset_basis\n\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i)\n hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)\n }\n\n // Return as a hex string\n return (hash >>> 0).toString(16)\n}\n\nexport function fastHash(obj: JsonObject, fields: string[]): string {\n if (fields.length === 0) return \"\"\n\n let combined = \"\"\n for (let i = 0; i < fields.length; i++) {\n const key = fields[i]\n if (!key) continue\n const value = obj[key]\n if (value !== undefined) {\n // Create a string representation with field position to avoid collision from reordering\n const str = typeof value === \"string\" ? value : cachedJsonStringify(value)\n combined += `${i}:${key}=${str}|`\n }\n }\n\n return combined ? fnv1aHash(combined) : \"\"\n}\n","import type {ArrayPlan} from \"../core/buildPlan\"\nimport type {JsonObject} from \"../types\"\n\n/**\n * Utility to create hash fields from a plan or infer them from objects\n */\nexport function getEffectiveHashFields(\n plan?: ArrayPlan,\n obj1?: JsonObject,\n obj2?: JsonObject,\n fallbackFields: string[] = [],\n): string[] {\n if (plan?.hashFields && plan.hashFields.length > 0) {\n return plan.hashFields\n }\n\n if (plan?.primaryKey) {\n return [plan.primaryKey]\n }\n\n if (fallbackFields.length > 0) {\n return fallbackFields\n }\n\n // Infer common fields from both objects\n if (obj1 && obj2) {\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n const commonKeys = keys1.filter((k) => keys2.includes(k))\n // Prioritize likely identifier fields\n const idFields = commonKeys.filter(\n (k) => k.includes(\"id\") || k.includes(\"key\") || k.includes(\"name\"),\n )\n return idFields.length > 0 ? idFields.slice(0, 3) : commonKeys.slice(0, 3)\n }\n\n return []\n}\n","import type {ArrayPlan} from \"../core/buildPlan\"\nimport type {JsonObject} from \"../types\"\nimport {fastHash} from \"./fashHash\"\nimport {getEffectiveHashFields} from \"./getEffectiveHashFields\"\n\nexport function getPlanFingerprint(plan?: ArrayPlan): string {\n if (!plan) return \"default\"\n return `${plan.primaryKey || \"\"}-${plan.hashFields?.join(\",\") || \"\"}-${plan.strategy || \"\"}`\n}\n\nexport function deepEqual(obj1: unknown, obj2: unknown): boolean {\n if (obj1 === obj2) return true;\n\n if (obj1 && obj2 && typeof obj1 === \"object\" && typeof obj2 === \"object\") {\n const arrA = Array.isArray(obj1);\n const arrB = Array.isArray(obj2);\n let i: number;\n let length: number;\n\n if (arrA && arrB) {\n const arr1 = obj1 as unknown[];\n const arr2 = obj2 as unknown[];\n length = arr1.length;\n if (length !== arr2.length) return false;\n for (i = length; i-- !== 0; )\n if (!deepEqual(arr1[i], arr2[i])) return false;\n return true;\n }\n\n if (arrA !== arrB) return false;\n\n const keys = Object.keys(obj1);\n length = keys.length;\n\n if (length !== Object.keys(obj2).length) return false;\n\n for (i = length; i-- !== 0; ) {\n const currentKey = keys[i] as string;\n if (currentKey !== undefined && !Object.hasOwn(obj2, currentKey))\n return false;\n if (!deepEqual((obj1 as JsonObject)[currentKey], (obj2 as JsonObject)[currentKey]))\n return false;\n }\n\n return true;\n }\n\n // Handle NaN case\n return Number.isNaN(obj1) && Number.isNaN(obj2);\n}\n\nconst eqCache = new WeakMap<object, WeakMap<object, boolean>>()\n// Enhanced cache for schema-aware equality with plan information\nconst schemaEqCache = new WeakMap<object, WeakMap<object, Map<string, boolean>>>()\n\nexport function deepEqualMemo(obj1: unknown, obj2: unknown, hotFields: string[] = []): boolean {\n // Fast reference equality check first\n if (obj1 === obj2) return true\n if (obj1 == null || obj2 == null) return obj1 === obj2\n\n const type1 = typeof obj1\n const type2 = typeof obj2\n if (type1 !== type2) return false\n if (type1 !== \"object\") {\n // primitives: fallback to strict equals (NaN handled above)\n return obj1 === obj2\n }\n\n // both are non-null objects\n const a = obj1 as JsonObject\n const b = obj2 as JsonObject\n\n // Fast path for empty objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n if (keysA.length === 0 && keysB.length === 0) return true\n if (keysA.length !== keysB.length) return false\n\n // Skip expensive hashing for very simple objects (≤ 3 keys)\n const shouldHash = hotFields.length > 0 && !Array.isArray(a) && !Array.isArray(b) && keysA.length > 3\n \n if (shouldHash) {\n // Only hash if object is complex enough to benefit\n const keyCount = keysA.length + keysB.length\n const estimatedSize = keyCount * 24\n if (estimatedSize >= 64 && keyCount > hotFields.length * 2) {\n const h1 = fastHash(a, hotFields)\n const h2 = fastHash(b, hotFields)\n if (h1 !== h2) return false\n }\n }\n\n // memoization cache\n let inner = eqCache.get(a)\n if (inner?.has(b)) return inner.get(b) ?? false\n\n // deep recursive compare (original implementation)\n const result = deepEqual(a, b)\n\n // store in cache\n if (!inner) {\n inner = new WeakMap()\n eqCache.set(a, inner)\n }\n inner.set(b, result)\n\n return result\n}\n\n/**\n * Schema-aware deep equality that prioritizes comparison of significant fields first\n * Uses plan information to optimize equality checks\n */\nexport function deepEqualSchemaAware(\n obj1: unknown,\n obj2: unknown,\n plan?: ArrayPlan,\n hotFields?: string[],\n): boolean {\n if (obj1 === obj2) return true\n if (obj1 == null || obj2 == null) return obj1 === obj2\n\n const type1 = typeof obj1\n const type2 = typeof obj2\n if (type1 !== type2) return false\n if (type1 !== \"object\") {\n return obj1 === obj2\n }\n\n const a = obj1 as JsonObject\n const b = obj2 as JsonObject\n\n // Use plan-derived hash fields for faster pre-filtering\n const effectiveHashFields = getEffectiveHashFields(plan, obj1, obj2, hotFields)\n\n // Enhanced hash-based pre-filtering with plan information\n if (effectiveHashFields.length > 0 && !Array.isArray(a) && !Array.isArray(b)) {\n const keyCount = Object.keys(a).length + Object.keys(b).length\n const estimatedSize = keyCount * 24\n // Skip hashing for tiny objects (< 64 bytes estimated)\n if (estimatedSize >= 64) {\n const h1 = fastHash(a, effectiveHashFields)\n const h2 = fastHash(b, effectiveHashFields)\n if (h1 !== h2) return false\n }\n }\n\n // Schema-aware memoization cache with plan fingerprint\n const planFingerprint = getPlanFingerprint(plan)\n\n let planCache = schemaEqCache.get(a)\n if (planCache?.has(b)) {\n const cached = planCache.get(b)?.get(planFingerprint)\n if (cached !== undefined) return cached\n }\n\n // Schema-aware comparison: check significant fields first\n if (plan?.requiredFields && plan.requiredFields.size > 0) {\n // Check required fields first - early exit if they differ\n for (const field of plan.requiredFields) {\n if (!deepEqual(a[field], b[field])) {\n // Cache the negative result\n if (!planCache) {\n planCache = new WeakMap()\n schemaEqCache.set(a, planCache)\n }\n if (!planCache.has(b)) {\n planCache.set(b, new Map())\n }\n planCache.get(b)?.set(planFingerprint, false)\n return false\n }\n }\n }\n\n // Check primary key field with high priority if available\n if (plan?.primaryKey && plan.primaryKey in a && plan.primaryKey in b) {\n const primaryKey = plan.primaryKey\n const keyEqual = deepEqual(a[primaryKey], b[primaryKey])\n if (!keyEqual) {\n // Cache the negative result\n if (!planCache) {\n planCache = new WeakMap()\n schemaEqCache.set(a, planCache)\n }\n if (!planCache.has(b)) {\n planCache.set(b, new Map())\n }\n planCache.get(b)?.set(planFingerprint, false)\n return false\n }\n }\n\n // Fall back to full deep equality check\n const result = deepEqual(a, b)\n\n // Cache the result with plan fingerprint\n if (!planCache) {\n planCache = new WeakMap()\n schemaEqCache.set(a, planCache)\n }\n if (!planCache.has(b)) {\n planCache.set(b, new Map())\n }\n planCache.get(b)?.set(planFingerprint, result)\n\n return result\n}\n","import type { ArrayPlan } from \"../core/buildPlan\";\nimport {\n deepEqual,\n deepEqualMemo,\n deepEqualSchemaAware,\n} from \"../performance/deepEqual\";\nimport { getEffectiveHashFields } from \"../performance/getEffectiveHashFields\";\nimport type { JsonArray, JsonObject, JsonValue, Operation } from \"../types\";\n\nexport type ModificationCallback = (\n item1: JsonValue,\n item2: JsonValue,\n path: string,\n patches: Operation[],\n skipEqualityCheck?: boolean\n) => void;\n\nexport function diffArrayByPrimaryKey(\n arr1: JsonArray,\n arr2: JsonArray,\n primaryKey: string,\n path: string,\n patches: Operation[],\n onModification: ModificationCallback,\n hashFields?: string[],\n plan?: ArrayPlan\n) {\n const effectiveHashFields = getEffectiveHashFields(\n plan,\n undefined,\n undefined,\n hashFields || []\n );\n const hashFieldsLength = effectiveHashFields.length;\n const hasHashFields = hashFieldsLength > 0;\n\n const arr1Length = arr1.length;\n const arr2Length = arr2.length;\n\n // Pre-allocate with exact sizes to avoid hidden class transitions\n const keyToIndex = new Map<string | number, number>();\n const itemsByIndex = new Array(arr1Length);\n const pathPrefix = path + \"/\";\n\n // Phase 1: Build index mappings - O(n)\n for (let i = 0; i < arr1Length; i++) {\n const item = arr1[i];\n if (typeof item === \"object\" && item !== null) {\n const keyValue = item[primaryKey as keyof typeof item];\n if (keyValue !== undefined && keyValue !== null) {\n const keyType = typeof keyValue;\n if (keyType === \"string\" || keyType === \"number\") {\n keyToIndex.set(keyValue as string | number, i);\n itemsByIndex[i] = item;\n }\n }\n }\n }\n\n const modificationPatches: Operation[] = [];\n const additionPatches: Operation[] = [];\n\n // Phase 2: Process arr2 and mark operations - O(m)\n for (let i = 0; i < arr2Length; i++) {\n const newItem = arr2[i];\n\n if (typeof newItem !== \"object\" || newItem === null) {\n continue;\n }\n\n const keyValue = newItem[primaryKey as keyof typeof newItem];\n if (keyValue === undefined) {\n continue;\n }\n\n const keyType = typeof keyValue;\n if (keyType !== \"string\" && keyType !== \"number\") {\n continue;\n }\n\n const oldIndex = keyToIndex.get(keyValue as string | number);\n if (oldIndex !== undefined) {\n // Delete immediately to avoid later lookup\n keyToIndex.delete(keyValue as string | number);\n\n const oldItem = itemsByIndex[oldIndex];\n let needsDiff = false;\n\n if (hasHashFields) {\n const oldItemObj = oldItem as JsonObject;\n const newItemObj = newItem as JsonObject;\n\n for (let j = 0; j < hashFieldsLength; j++) {\n const field = effectiveHashFields[j];\n // Short-circuit evaluation optimized\n if (field && oldItemObj[field] !== newItemObj[field]) {\n needsDiff = true;\n break;\n }\n }\n\n // Only expensive deep equal if hash fields match\n if (!needsDiff && oldItem !== newItem) {\n needsDiff = !deepEqual(oldItem, newItem);\n }\n } else if (plan) {\n needsDiff = !deepEqualSchemaAware(\n oldItem,\n newItem,\n plan,\n effectiveHashFields\n );\n } else {\n // Reference equality first (fastest path)\n needsDiff = oldItem !== newItem && !deepEqual(oldItem, newItem);\n }\n\n if (needsDiff) {\n const itemPath = pathPrefix + oldIndex;\n onModification(oldItem, newItem, itemPath, modificationPatches, true);\n }\n } else {\n additionPatches.push({\n op: \"add\",\n path: pathPrefix + \"-\",\n value: newItem,\n });\n }\n }\n\n // Phase 3: Generate removal patches directly - O(remaining items)\n const removalIndices = Array.from(keyToIndex.values());\n\n // O(k log k)) where k << n and k and n are the number of removals and items in the array respectively\n removalIndices.sort((a, b) => b - a);\n\n const removalPatches: Operation[] = new Array(removalIndices.length);\n\n for (let i = 0; i < removalIndices.length; i++) {\n const index = removalIndices[i] as number;\n removalPatches[i] = {\n op: \"remove\",\n path: pathPrefix + index,\n oldValue: itemsByIndex[index],\n };\n }\n\n const totalPatches =\n modificationPatches.length + removalPatches.length + additionPatches.length;\n if (totalPatches > 0) {\n patches.push(...modificationPatches, ...removalPatches, ...additionPatches);\n }\n}\n\nexport function diffArrayLCS(\n arr1: JsonArray,\n arr2: JsonArray,\n path: string,\n patches: Operation[],\n onModification: ModificationCallback,\n hashFields?: string[],\n plan?: ArrayPlan\n) {\n const effectiveHashFields = getEffectiveHashFields(\n plan,\n undefined,\n undefined,\n hashFields || []\n );\n\n const n = arr1.length;\n const m = arr2.length;\n\n // Early exit for empty arrays\n if (n === 0) {\n const prefixPath = path === \"\" ? \"/\" : path + \"/\";\n for (let i = 0; i < m; i++) {\n patches.push({\n op: \"add\",\n path: prefixPath + i,\n value: arr2[i] as JsonValue,\n });\n }\n return;\n }\n if (m === 0) {\n for (let i = n - 1; i >= 0; i--) {\n patches.push({\n op: \"remove\",\n path: path === \"\" ? \"/\" : path + \"/\" + i,\n });\n }\n return;\n }\n\n const max = n + m;\n const offset = max;\n const bufSize = 2 * max + 1;\n\n // Pre-allocate buffers to avoid repeated allocations\n const buffer1 = new Int32Array(bufSize);\n const buffer2 = new Int32Array(bufSize);\n buffer1.fill(-1);\n buffer2.fill(-1);\n\n let vPrev = buffer1;\n let vCurr = buffer2;\n vPrev[offset + 1] = 0;\n\n // Pre-allocate trace array with estimated size\n const trace = new Array(max + 1);\n let traceLen = 0;\n let endD = -1;\n\n // Cache equality checks to avoid redundant comparisons\n const equalCache = new Map<number, boolean>();\n const cacheKey = (x: number, y: number): number => (x << 16) | y; // Assumes arrays < 65536 length\n\n const equalAt = (x: number, y: number): boolean => {\n const key = cacheKey(x, y);\n let result = equalCache.get(key);\n if (result !== undefined) return result;\n\n result = plan\n ? deepEqualSchemaAware(arr1[x], arr2[y], plan, effectiveHashFields)\n : deepEqualMemo(arr1[x], arr2[y], effectiveHashFields);\n\n equalCache.set(key, result);\n return result;\n };\n\n const prefixPath = path === \"\" ? \"/\" : path + \"/\";\n\n // Forward pass with optimizations\n outer: for (let d = 0; d <= max; d++) {\n // Clone only the used portion of the array\n const traceCopy = new Int32Array(bufSize);\n traceCopy.set(vPrev);\n trace[traceLen++] = traceCopy;\n\n const dMin = -d;\n const dMax = d;\n\n for (let k = dMin; k <= dMax; k += 2) {\n const kOffset = k + offset;\n\n // Inline get() for performance\n const vLeft = kOffset > 0 ? (vPrev[kOffset - 1] as number) : -1;\n const vRight =\n kOffset < bufSize - 1 ? (vPrev[kOffset + 1] as number) : -1;\n\n const down = k === dMin || (k !== dMax && vLeft < vRight);\n let x = down ? vRight : vLeft + 1;\n let y = x - k;\n\n // Snake with bounds checking\n while (x < n && y < m && equalAt(x, y)) {\n x++;\n y++;\n }\n\n vCurr[kOffset] = x;\n\n if (x >= n && y >= m) {\n const finalCopy = new Int32Array(bufSize);\n finalCopy.set(vCurr);\n trace[traceLen++] = finalCopy;\n endD = d;\n break outer;\n }\n }\n\n // Swap buffers efficiently\n const tmp = vPrev;\n vPrev = vCurr;\n vCurr = tmp;\n vCurr.fill(-1);\n }\n\n if (endD === -1) return;\n\n // Backtracking to build edit script\n const editScript: Array<{\n op: \"common\" | \"remove\" | \"add\";\n ai?: number;\n bi?: number;\n }> = [];\n\n let x = n;\n let y = m;\n\n for (let d = endD; d > 0; d--) {\n const vRow = trace[d];\n const k = x - y;\n const kOffset = k + offset;\n\n const vLeft = kOffset > 0 ? vRow[kOffset - 1] : -1;\n const vRight = kOffset < bufSize - 1 ? vRow[kOffset + 1] : -1;\n\n const down = k === -d || (k !== d && vLeft < vRight);\n const prevK = down ? k + 1 : k - 1;\n const prevX = vRow[prevK + offset];\n const prevY = prevX - prevK;\n\n // Add common elements (snake)\n while (x > prevX && y > prevY) {\n x--;\n y--;\n editScript.push({ op: \"common\", ai: x, bi: y });\n }\n\n // Add the edit operation\n if (down) {\n y--;\n editScript.push({ op: \"add\", bi: y });\n } else {\n x--;\n editScript.push({ op: \"remove\", ai: x });\n }\n }\n\n // Add remaining common elements\n while (x > 0 && y > 0) {\n x--;\n y--;\n editScript.push({ op: \"common\", ai: x, bi: y });\n }\n\n // Reverse to get forward order\n editScript.reverse();\n\n // Optimize: collapse adjacent remove+add into replace operations\n const optimizedScript: Array<{\n op: \"common\" | \"remove\" | \"add\" | \"replace\";\n ai?: number;\n bi?: number;\n }> = [];\n\n for (let i = 0; i < editScript.length; i++) {\n const current = editScript[i];\n const next = editScript[i + 1];\n\n // Check if we can combine remove + add into replace\n if (\n current &&\n current.op === \"remove\" &&\n next &&\n next.op === \"add\" &&\n current.ai !== undefined &&\n next.bi !== undefined\n ) {\n optimizedScript.push({ op: \"replace\", ai: current.ai, bi: next.bi });\n i++; // Skip the next operation\n } else if (current) {\n optimizedScript.push(current);\n }\n }\n\n // Apply operations and generate patches\n let currentIndex = 0;\n\n for (const operation of optimizedScript) {\n switch (operation.op) {\n case \"common\": {\n const v1 = arr1[operation.ai as number];\n const v2 = arr2[operation.bi as number];\n // Only call onModification for objects that might have nested differences\n if (\n typeof v1 === \"object\" &&\n v1 !== null &&\n typeof v2 === \"object\" &&\n v2 !== null\n ) {\n onModification(v1, v2, prefixPath + currentIndex, patches, false);\n }\n currentIndex++;\n break;\n }\n case \"replace\": {\n patches.push({\n op: \"replace\",\n path: prefixPath + currentIndex,\n value: arr2[operation.bi as number] as JsonValue,\n oldValue: arr1[operation.ai as number],\n });\n currentIndex++;\n break;\n }\n case \"remove\": {\n patches.push({\n op: \"remove\",\n path: prefixPath + currentIndex,\n oldValue: arr1[operation.ai as number],\n });\n // Don't increment currentIndex for removes\n break;\n }\n case \"add\": {\n patches.push({\n op: \"add\",\n path: prefixPath + currentIndex,\n value: arr2[operation.bi as number] as JsonValue,\n });\n currentIndex++;\n break;\n }\n }\n }\n}\n\nexport function diffArrayUnique(\n arr1: JsonArray,\n arr2: JsonArray,\n path: string,\n patches: Operation[]\n) {\n const n = arr1.length;\n const m = arr2.length;\n const pathPrefix = path + \"/\";\n\n const patches_temp: Operation[] = [];\n\n if (n === 0 && m === 0) return;\n if (n === 0) {\n // All additions\n for (let i = 0; i < m; i++) {\n patches_temp.push({ op: \"add\", path: pathPrefix + \"-\", value: arr2[i] });\n }\n patches.push(...patches_temp);\n return;\n }\n if (m === 0) {\n // All removals (descending order)\n for (let i = n - 1; i >= 0; i--) {\n patches_temp.push({\n op: \"remove\",\n path: pathPrefix + i,\n oldValue: arr1[i],\n });\n }\n patches.push(...patches_temp);\n return;\n }\n\n // Use Map for O(1) lookups instead of Set for complex logic\n const arr1Map = new Map<JsonValue, number>();\n const arr2Map = new Map<JsonValue, number>();\n\n // Single pass to build both maps\n for (let i = 0; i < n; i++) {\n arr1Map.set(arr1[i] as JsonValue, i);\n }\n for (let i = 0; i < m; i++) {\n arr2Map.set(arr2[i] as JsonValue, i);\n }\n\n const minLength = Math.min(n, m);\n const replacedItems = new Set<JsonValue>();\n\n // Phase 1: Handle replacements in common indices - O(min(n,m))\n for (let i = 0; i < minLength; i++) {\n const val1 = arr1[i];\n const val2 = arr2[i];\n\n if (val1 !== val2) {\n patches_temp.push({\n op: \"replace\",\n path: pathPrefix + i,\n value: val2,\n oldValue: val1,\n });\n replacedItems.add(val2 as JsonValue);\n }\n }\n\n // Phase 2: Handle removals - O(n)\n // Collect removal indices first, then sort\n const removalIndices: number[] = [];\n\n for (let i = n - 1; i >= 0; i--) {\n const item = arr1[i];\n\n // Skip if this position was replaced or item exists in arr2\n if (i < minLength && arr1[i] !== arr2[i]) {\n continue;\n }\n\n if (!arr2Map.has(item as JsonValue)) {\n removalIndices.push(i);\n }\n }\n\n // Add removal patches (already in descending order)\n for (const index of removalIndices) {\n patches_temp.push({\n op: \"remove\",\n path: pathPrefix + index,\n oldValue: arr1[index],\n });\n }\n\n // Phase 3: Handle additions - O(m)\n for (let i = 0; i < m; i++) {\n const item = arr2[i];\n\n // Skip if this was a replacement\n if (i < minLength && arr1[i] !== arr2[i]) {\n continue;\n }\n\n if (!arr1Map.has(item as JsonValue)) {\n patches_temp.push({ op: \"add\", path: pathPrefix + \"-\", value: item });\n }\n }\n\n patches.push(...patches_temp);\n}\n\nexport function checkArraysUnique(arr1: JsonArray, arr2: JsonArray): boolean {\n const len1 = arr1.length;\n const len2 = arr2.length;\n\n if (len1 !== len2) return false;\n\n const seen1 = new Set<JsonValue>();\n const seen2 = new Set<JsonValue>();\n\n for (let i = 0; i < len1; i++) {\n const val1 = arr1[i];\n const val2 = arr2[i];\n\n if (seen1.has(val1 as JsonValue) || seen2.has(val2 as JsonValue)) {\n return false;\n }\n\n seen1.add(val1 as JsonValue);\n seen2.add(val2 as JsonValue);\n }\n\n return true;\n}\n","import type {JsonObject, JsonValue} from \"../types\"\n\n/**\n * Cache for path resolution results to avoid repeated computations\n */\nconst pathResolutionCache = new Map<string, WeakMap<object, JsonValue | undefined>>()\n\n/**\n * Cache for normalized paths to avoid repeated regex operations\n */\nconst normalizedPathCache = new Map<string, string>()\n\n/**\n * Resolves a JSON Pointer path to get a value from an object\n * Handles JSON Pointer escaping (~0 for ~, ~1 for /)\n */\nexport function getValueByPath<T = JsonValue>(obj: JsonValue, path: string): T | undefined {\n if (path === \"\") return obj as T\n\n // Check cache first\n let objCache = pathResolutionCache.get(path)\n if (!objCache) {\n objCache = new WeakMap()\n pathResolutionCache.set(path, objCache)\n }\n\n if (typeof obj === \"object\" && obj !== null && objCache.has(obj)) {\n return objCache.get(obj) as T | undefined\n }\n\n const parts = path.split(\"/\").slice(1)\n let current: JsonValue = obj\n\n for (const part of parts) {\n if (typeof current !== \"object\" || current === null) {\n if (typeof obj === \"object\" && obj !== null) {\n objCache.set(obj, undefined)\n }\n return undefined\n }\n\n const key = unescapeJsonPointer(part)\n\n if (Array.isArray(current)) {\n const index = Number.parseInt(key, 10)\n if (Number.isNaN(index) || index < 0 || index >= current.length) {\n if (typeof obj === \"object\" && obj !== null) {\n objCache.set(obj, undefined)\n }\n return undefined\n }\n current = current[index] as JsonValue\n } else {\n const objCurrent = current as JsonObject\n if (!Object.hasOwn(objCurrent, key)) {\n if (typeof obj === \"object\" && obj !== null) {\n objCache.set(obj, undefined)\n }\n return undefined\n }\n current = objCurrent[key] as JsonValue\n }\n }\n\n if (typeof obj === \"object\" && obj !== null) {\n objCache.set(obj, current)\n }\n\n return current as T\n}\n\n/**\n * Resolves a patch path, handling special cases like \"/-\" for array append operations\n */\nexport function resolvePatchPath(\n path: string,\n jsonObj: JsonValue,\n isForNewVersion = false,\n): string | null {\n if (path.endsWith(\"/-\")) {\n const parentPath = path.slice(0, -2)\n\n if (parentPath === \"\") {\n if (Array.isArray(jsonObj)) {\n return isForNewVersion ? `/${jsonObj.length - 1}` : `/${jsonObj.length}`\n }\n return null\n }\n\n const parentValue = getValueByPath(jsonObj, parentPath)\n if (Array.isArray(parentValue)) {\n return isForNewVersion ? `${parentPath}/${parentValue.length - 1}` : parentPath\n }\n }\n\n return path\n}\n\n/**\n * Normalizes a path by removing array indices (e.g., /items/0/name -> /items/name)\n * Optimized to avoid regex for simple cases\n */\nexport function normalizePath(path: string): string {\n if (normalizedPathCache.has(path)) {\n return normalizedPathCache.get(path) as string\n }\n\n // Fast path: if no digits, no normalization needed\n if (!/\\d/.test(path)) {\n normalizedPathCache.set(path, path)\n return path\n }\n\n // For paths with digits, use optimized replacement\n const normalized = path.replace(/\\/\\d+/g, \"\")\n normalizedPathCache.set(path, normalized)\n return normalized\n}\n\n/**\n * Gets the parent path and generates a wildcard version\n */\nexport function getWildcardPath(path: string): string | null {\n const normalizedPath = normalizePath(path)\n const lastSlash = normalizedPath.lastIndexOf(\"/\")\n\n if (lastSlash >= 0) {\n return `${normalizedPath.substring(0, lastSlash)}/*`\n }\n\n return null\n}\n\n/**\n * Unescapes JSON Pointer special characters\n * ~1 becomes /, ~0 becomes ~\n */\nexport function unescapeJsonPointer(part: string): string {\n return part.replace(/~1/g, \"/\").replace(/~0/g, \"~\")\n}\n\n/**\n * Escapes JSON Pointer special characters\n * / becomes ~1, ~ becomes ~0\n */\nexport function escapeJsonPointer(part: string): string {\n return part.replace(/~/g, \"~0\").replace(/\\//g, \"~1\")\n}\n\n/**\n * Splits a path into its component parts, handling escaping\n */\nexport function splitPath(path: string): string[] {\n if (path === \"\") return []\n return path.split(\"/\").slice(1).map(unescapeJsonPointer)\n}\n\n/**\n * Joins path parts into a JSON Pointer path, handling escaping\n */\nexport function joinPath(parts: string[]): string {\n if (parts.length === 0) return \"\"\n return `/${parts.map(escapeJsonPointer).join(\"/\")}`\n}\n","import type {JsonObject} from \"../types\"\n\nexport interface JSONSchema extends JsonObject {\n $ref?: string\n type?: string | string[]\n properties?: Record<string, JSONSchema>\n additionalProperties?: boolean | JSONSchema\n items?: JSONSchema\n anyOf?: JSONSchema[]\n oneOf?: JSONSchema[]\n allOf?: JSONSchema[]\n required?: string[]\n}\n\ntype Schema = JSONSchema\n\nexport interface ArrayPlan {\n primaryKey: string | null\n // Pre-resolved item schema to avoid repeated $ref resolution\n itemSchema?: JSONSchema\n // Set of required fields for faster validation and comparison\n requiredFields?: Set<string>\n // Fields to use for quick equality hashing before deep comparison\n hashFields?: string[]\n // Strategy hint for array comparison\n strategy?: \"primaryKey\" | \"lcs\" | \"unique\"\n}\n\nexport type Plan = Map<string, ArrayPlan>\n\nexport interface BuildPlanOptions {\n schema: Schema\n primaryKeyMap?: Record<string, string>\n basePath?: string\n}\n\nexport function _resolveRef(ref: string, schema: Schema): JSONSchema | null {\n if (!ref.startsWith(\"#/\")) {\n // We only support local references for now.\n console.warn(`Unsupported reference: ${ref}`)\n return null\n }\n const path = ref.substring(2).split(\"/\")\n let current: unknown = schema\n for (const part of path) {\n if (typeof current !== \"object\" || current === null || !Object.hasOwn(current, part)) {\n return null\n }\n current = (current as Record<string, unknown>)[part]\n }\n return current as JSONSchema\n}\n\nexport function _traverseSchema(\n subSchema: JSONSchema | boolean,\n docPath: string,\n plan: Plan,\n schema: Schema,\n visited: Set<object> = new Set(),\n options?: Omit<BuildPlanOptions, \"schema\">,\n) {\n if (!subSchema || typeof subSchema !== \"object\" || visited.has(subSchema)) {\n return\n }\n visited.add(subSchema)\n\n if (subSchema.$ref) {\n const resolved = _resolveRef(subSchema.$ref, schema)\n if (resolved) {\n // Note: We don't change the docPath when resolving a ref\n _traverseSchema(resolved, docPath, plan, schema, visited, options)\n }\n // The visited check at the start of the function handles cycles.\n // We should remove the subSchema from visited before returning,\n // so it can be visited again via a different path.\n visited.delete(subSchema)\n return\n }\n\n for (const keyword of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const schemas = subSchema[keyword]\n if (schemas && Array.isArray(schemas)) {\n const seenFingerprints = new Set<string>()\n for (const s of schemas) {\n const fp = stableStringify(s)\n if (seenFingerprints.has(fp)) continue // skip duplicate branch\n seenFingerprints.add(fp)\n _traverseSchema(s, docPath, plan, schema, visited, options)\n }\n }\n }\n\n if (subSchema.type === \"object\") {\n if (subSchema.properties) {\n for (const key in subSchema.properties) {\n _traverseSchema(\n subSchema.properties[key] as JSONSchema,\n `${docPath}/${key}`,\n plan,\n schema,\n visited,\n options,\n )\n }\n }\n if (typeof subSchema.additionalProperties === \"object\" && subSchema.additionalProperties) {\n _traverseSchema(\n subSchema.additionalProperties,\n `${docPath}/*`,\n plan,\n schema,\n visited,\n options,\n )\n }\n }\n\n if (subSchema.type === \"array\" && subSchema.items) {\n const arrayPlan: ArrayPlan = {primaryKey: null, strategy: \"lcs\"}\n\n let itemsSchema = subSchema.items\n if (itemsSchema.$ref) {\n itemsSchema = _resolveRef(itemsSchema.$ref, schema) || itemsSchema\n }\n\n // Store the resolved item schema to avoid repeated resolution\n arrayPlan.itemSchema = itemsSchema\n\n // Check if items are primitives\n const isPrimitive =\n itemsSchema &&\n (itemsSchema.type === \"string\" ||\n itemsSchema.type === \"number\" ||\n itemsSchema.type === \"boolean\")\n\n if (isPrimitive) {\n arrayPlan.strategy = \"unique\"\n }\n\n const customKey = options?.primaryKeyMap?.[docPath]\n if (customKey) {\n arrayPlan.primaryKey = customKey\n arrayPlan.strategy = \"primaryKey\"\n } else if (!isPrimitive) {\n // Find primary key and other metadata only for non-primitive object arrays\n const findMetadata = (\n s: JSONSchema,\n ): Pick<ArrayPlan, \"primaryKey\" | \"requiredFields\" | \"hashFields\"> | null => {\n let currentSchema = s\n if (!currentSchema || typeof currentSchema !== \"object\") return null\n\n if (currentSchema.$ref) {\n const resolved = _resolveRef(currentSchema.$ref, schema)\n if (!resolved) return null\n currentSchema = resolved\n }\n if (!currentSchema || currentSchema.type !== \"object\" || !currentSchema.properties) {\n return null\n