const richTypes = ["Date", "RegExp", "String", "Number"]; const temporalTypes = Object.getOwnPropertyNames(globalThis.Temporal || {}); export default function diff(obj, newObj, options = { cyclesFix: true }, _stack = []) { let diffs = []; const isObjArray = Array.isArray(obj); for (const key in obj) { const value = obj[key]; const path = isObjArray ? +key : key; if (!(key in newObj)) { diffs.push({ type: "REMOVE", path: [path], oldValue: value, }); continue; } const newValue = newObj[key]; const areCompatibleObjects = typeof value === "object" && typeof newValue === "object" && Array.isArray(value) === Array.isArray(newValue); // Only compute for non-null objects — primitives and null skip this // entirely since Object.getPrototypeOf is expensive to call on every key const objConstructor = areCompatibleObjects && value ? Object.getPrototypeOf(value)?.constructor?.name : undefined; if (value && newValue && areCompatibleObjects && !richTypes.includes(objConstructor) && !temporalTypes.includes(objConstructor) && (!options.cyclesFix || !_stack.includes(value))) { // Recurse into objects and arrays if (options.cyclesFix) { _stack.push(value); } const subDiffs = diff(value, newValue, options, _stack); if (options.cyclesFix) { _stack.pop(); } for (const subDiff of subDiffs) { subDiff.path.unshift(path); diffs.push(subDiff); } } else if (!(Object.is(value, newValue) || (temporalTypes.includes(objConstructor) && String(value) === String(newValue)) || (richTypes.includes(objConstructor) && (isNaN(value) ? value + "" === newValue + "" : +value === +newValue)))) { diffs.push({ path: [path], type: "CHANGE", value: newValue, oldValue: value, }); } } const isNewObjArray = Array.isArray(newObj); for (const key in newObj) { if (!(key in obj)) { diffs.push({ type: "CREATE", path: [isNewObjArray ? +key : key], value: newObj[key], }); } } return diffs; }