/** * @module db/stableEqual * @description Key-order-insensitive deep equality, used by {@link Brainy.diff} * to decide whether an id present at both endpoints actually *changed* value. * * A naive `JSON.stringify(a) === JSON.stringify(b)` is wrong for this: object key * order is insignificant for stored-metadata equality, but `stringify` is * order-sensitive, so `{a:1,b:2}` and `{b:2,a:1}` would mis-compare as changed and * a no-op re-write would show up as a spurious `modified`. This walks both values * structurally — object keys compared as a set, array elements compared in order * (arrays ARE order-significant) — over the JSON-shaped values that live in * generation records. */ /** * @description Deep-equal two JSON-shaped values, insensitive to object key order. * @param a - First value. * @param b - Second value. * @returns `true` if structurally equal (key order ignored; array order honored; * `NaN` equals `NaN`). * @example * stableDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) // → true * stableDeepEqual([1, 2], [2, 1]) // → false (order matters) */ export function stableDeepEqual(a: unknown, b: unknown): boolean { if (a === b) return true // NaN is never === itself, but for a stable value comparison NaN equals NaN. if (typeof a === 'number' && typeof b === 'number') { return Number.isNaN(a) && Number.isNaN(b) } // Distinct primitives, or exactly one of them null/non-object → not equal // (the `a === b` above already returned true for equal primitives and for // both-null). if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { return false } const aIsArr = Array.isArray(a) const bIsArr = Array.isArray(b) if (aIsArr !== bIsArr) return false if (aIsArr) { const aa = a as unknown[] const ba = b as unknown[] if (aa.length !== ba.length) return false for (let i = 0; i < aa.length; i++) { if (!stableDeepEqual(aa[i], ba[i])) return false } return true } const ao = a as Record const bo = b as Record const aKeys = Object.keys(ao) const bKeys = Object.keys(bo) if (aKeys.length !== bKeys.length) return false for (const k of aKeys) { if (!Object.prototype.hasOwnProperty.call(bo, k)) return false if (!stableDeepEqual(ao[k], bo[k])) return false } return true }