65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
|
|
/**
|
||
|
|
* @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<string, unknown>
|
||
|
|
const bo = b as Record<string, unknown>
|
||
|
|
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
|
||
|
|
}
|