deep-object-compare-trace
v0.1.0
Published
Dependency-free deep equality for complex JavaScript values with explainable traces and timing.
Maintainers
Readme
English | Русский
deep-object-compare-trace
Глубокое сравнение JavaScript-значений и объектных графов с точной семантикой, структурированным трейсом, контролем ресурсов и готовым консольным отчётом.
import { deepCompare, deepEqual } from "deep-object-compare-trace";
deepEqual(
{ roles: new Set(["reader", "admin"]) },
{ roles: new Set(["admin", "reader"]) },
); // true
const result = deepCompare(
{ user: { name: "Ada", role: "editor" } },
{ user: { name: "Ada", role: "admin" } },
{ trace: { mode: "failures" }, timing: true },
);
result.status; // "NOT_EQUAL"
result.trace[0]?.path; // "$.user.role"
result.durationMs; // например, 0.284Библиотека не использует runtime-зависимости и не делегирует сравнение Lodash-подобным пакетам. Поставляются ESM, CommonJS, TypeScript declarations и source maps.
Возможности
- циклические и очень глубокие графы без рекурсивного обхода обычных объектов;
- проверка топологии ссылок: одна общая ссылка отличается от двух независимых клонов;
- неупорядоченное структурное сопоставление
MapиSet; - точное сравнение
ArrayBuffer,SharedArrayBuffer,DataView, typed arrays и Node.jsBuffer; - безопасное сравнение property descriptors без вызова пользовательских getters;
- symbol- и non-enumerable-свойства, разреженные массивы и дополнительные поля встроенных объектов;
Date,RegExp,Error,AggregateError, boxed primitives,URL,URLSearchParams;- пять исходов вместо неоднозначного boolean:
EQUAL,NOT_EQUAL,INDETERMINATE,ABORTED,ERROR; - структурированный trace с точными путями, кодами причин, redaction и ограничением памяти;
- статистика работы, раздельный timing и красивый ANSI-отчёт;
- лимиты глубины, узлов, операций, различий, времени и сложности
Map/Set; - синхронные custom comparators для доменных типов;
- защита от исключений враждебных/отозванных
Proxy, отсоединённых буферов и некорректных callbacks.
Содержание
- Установка
- Быстрый старт
- Какой API выбрать
- Контракт статусов
- Параметры сравнения
- Поддерживаемые типы
- Графы, циклы и aliasing
- Map и Set
- Бинарные данные
- Дескрипторы, getters и прототипы
- Трейс, пути и форматирование
- Структура результата
- Custom comparators
- Практические рецепты
- Производительность и сложность
- Безопасность и ограничения
- Разработка
Установка
npm install deep-object-compare-tracepnpm add deep-object-compare-traceyarn add deep-object-compare-traceТребуется Node.js 22 или новее. ESM:
import {
assertDeepEqual,
deepCompare,
deepEqual,
formatComparison,
} from "deep-object-compare-trace";CommonJS:
const {
assertDeepEqual,
deepCompare,
deepEqual,
formatComparison,
} = require("deep-object-compare-trace");Экспорт по умолчанию отсутствует. Современные браузерные сборщики могут использовать ESM-сборку; фактическая поддержка конкретных встроенных типов зависит от среды выполнения. Целевой стандарт сборки — ES2022.
Быстрый старт
Только boolean
import { deepEqual } from "deep-object-compare-trace";
const left = {
id: 42,
tags: new Set(["typescript", "graph"]),
metadata: new Map([[{ locale: "ru" }, { enabled: true }]]),
};
const right = {
metadata: new Map([[{ locale: "ru" }, { enabled: true }]]),
tags: new Set(["graph", "typescript"]),
id: 42,
};
deepEqual(left, right); // trueДиагностируемый результат
import { deepCompare } from "deep-object-compare-trace";
const result = deepCompare(
{ user: { id: 7, permissions: ["read", "write"] } },
{ user: { id: 7, permissions: ["read", "admin"] } },
{
executionMode: "first-difference",
trace: { mode: "failures", maxEntries: 20 },
timing: true,
},
);
if (result.status === "NOT_EQUAL") {
const failure = result.trace.find((entry) => entry.status === "failed");
console.log(failure?.path); // $.user.permissions[1]
console.log(failure?.code); // PRIMITIVE_MISMATCH
console.log(failure?.message); // Primitive values differ.
}Готовый консольный отчёт
deepCompare(actual, expected, {
trace: { mode: "failures" },
timing: true,
output: true,
colors: "auto",
});Библиотека ничего не печатает без output: true. Для полного контроля можно вызвать formatComparison() самостоятельно.
Какой API выбрать
deepEqual(actual, expected, options?): boolean
Самый быстрый публичный путь, когда нужен только boolean. Метод всегда использует short-circuit режим и прекращает работу после первого доказанного расхождения.
if (deepEqual(cacheValue, expectedValue)) {
reuseCache();
}Важно: false здесь означает «результат не EQUAL». В boolean сворачиваются четыре разных состояния: настоящее неравенство, нехватка ресурсов, отмена и ошибка безопасной интроспекции. Если это различие важно, используйте deepCompare().
Переданный в deepEqual() параметр executionMode намеренно переопределяется значением "fast". При output: true метод всё равно строит и печатает полноценный результат, но возвращает только boolean.
deepCompare(actual, expected, options?): CompareResult
Основной API для production-использования. Возвращает статус, trace, статистику, причины остановки и, по запросу, timing.
const result = deepCompare(untrustedActual, expected, {
executionMode: "first-difference",
trace: { mode: "failures" },
maxDepth: 256,
maxNodes: 100_000,
maxOperations: 2_000_000,
timeoutMs: 100,
});
switch (result.status) {
case "EQUAL":
break;
case "NOT_EQUAL":
reportMismatch(result);
break;
case "INDETERMINATE":
case "ABORTED":
case "ERROR":
reportIncompleteComparison(result);
break;
}formatComparison(result, options?): string
Создаёт консольный отчёт без повторного сравнения и ничего не печатает самостоятельно.
const result = deepCompare(actual, expected, {
trace: { mode: "failures" },
timing: true,
});
const report = formatComparison(result, {
colors: "never",
includePassed: false,
maxEntries: 50,
maxValueLength: 120,
});
logger.info(report);assertDeepEqual(actual, expected, options?): asserts actual is TExpected
Ничего не возвращает при EQUAL. Для любого другого статуса выбрасывает DeepComparisonError, содержащий полный CompareResult.
import {
assertDeepEqual,
DeepComparisonError,
} from "deep-object-compare-trace";
try {
assertDeepEqual(actual, expected, {
trace: { mode: "failures" },
timing: true,
});
} catch (error) {
if (error instanceof DeepComparisonError) {
console.error(error.message); // отформатированный отчёт без ANSI
console.error(error.result.status); // точный статус
}
throw error;
}DeepComparisonError можно создать и вручную:
const result = deepCompare(actual, expected, { trace: true });
if (!result.equal) {
throw new DeepComparisonError(result, {
colors: "never",
includePassed: false,
maxEntries: 50,
});
}Конструктор принимает CompareResult и необязательные RenderOptions. Поле error.result сохраняет исходный структурированный результат, а error.message содержит вывод formatComparison(). По умолчанию ANSI-цвета в исключении отключены.
Контракт статусов
| Статус | equal | Значение |
|---|---:|---|
| EQUAL | true | Полный выполненный обход доказал равенство по выбранным политикам. |
| NOT_EQUAL | false | Обнаружено хотя бы одно доказанное расхождение. |
| INDETERMINATE | false | Сравнение нельзя завершить: исчерпан лимит или встретился неподдерживаемый непрозрачный тип. Это не доказательство неравенства. |
| ABORTED | false | Работа кооперативно отменена через AbortSignal. Только для этого статуса aborted === true. |
| ERROR | false | Proxy trap, reflection или custom comparator выбросил исключение либо нарушил контракт. Исключение преобразовано в результат. |
Никогда не интерпретируйте любой result.equal === false как подтверждённое неравенство. Проверяйте result.status === "NOT_EQUAL".
Поля reason и deprecated-алиас abortReason появляются для INDETERMINATE, ABORTED и ERROR.
Параметры сравнения
Все параметры необязательны. В таблицах указаны значения по умолчанию для deepCompare(). deepEqual() принудительно использует executionMode: "fast".
Семантика equality
| Параметр | По умолчанию | Описание |
|---|---|---|
| equalityMode | "graph" | "graph" требует одинаковой topology ссылок. "structural" сравнивает развёрнутую структуру и игнорирует кратность aliases, но всё равно безопасно замыкает циклы. |
| primitiveMode | "same-value" | Отношение равенства примитивов: SameValue, SameValueZero или strict equality. Подробности ниже. |
| executionMode | "all-differences" | Стратегия остановки и диагностики: fast, first-difference, all-differences, full-trace. |
| mapKeyMode | "deep" | "deep" структурно сопоставляет ключи Map; "identity" требует identity/SameValueZero ключа. |
| binaryMode | "exact" | "exact" учитывает offset, длину, полный backing buffer и его topology; "view" сравнивает видимое byte window. |
| strictPrototypes | true | Требует identity прототипов, включая identity класса. Отключайте для осознанного shape-only или cross-realm сравнения. |
| classInstanceMode | "opaque" | "opaque" возвращает INDETERMINATE для разных экземпляров пользовательских классов, поскольку private/internal state недоступен reflection. "properties" явно сравнивает только наблюдаемые properties и прототипы. |
| comparePropertyDescriptors | false | Дополнительно сравнивает writable, enumerable и configurable. Data/accessor вид и getter/setter references проверяются всегда. |
| includeNonEnumerable | true | Включает non-enumerable own properties. |
| includeSymbols | true | Включает symbol-keyed own properties. |
| compareRegExpState | true | Кроме source и flags сравнивает RegExp.lastIndex. |
| compareObjectIntegrity | false | Сравнивает Object.isExtensible, Object.isSealed и Object.isFrozen. |
| compareErrorStack | false | Сравнивает Error.stack. Отключено, потому что stack зависит от места создания и в V8 может быть lazy accessor. |
| customComparators | [] | Упорядоченный список синхронных доменных компараторов. Первый canCompare() === true владеет парой. |
Режимы примитивов:
| primitiveMode | NaN и NaN | +0 и -0 | Эквивалент |
|---|---:|---:|---|
| "same-value" | равны | различаются | Object.is |
| "same-value-zero" | равны | равны | семантика ключей Map/Set |
| "strict" | различаются | равны | оператор === |
Режимы выполнения:
| executionMode | Поведение |
|---|---|
| "fast" | Остановка после первого доказанного различия; сбор trace зависит от trace. Используется deepEqual(). |
| "first-difference" | Та же граница в одно различие, но явно выражает диагностический сценарий deepCompare(). |
| "all-differences" | Продолжает обход и собирает все найденные различия до лимитов. Default для deepCompare(). |
| "full-trace" | Продолжает обход, автоматически включает trace и успешные записи. Самый подробный и дорогой режим. |
maxDifferences дополнительно ограничивает all-differences и full-trace. В fast и first-difference эффективный предел всегда равен одному.
Trace, timing и вывод
| Параметр | По умолчанию | Описание |
|---|---|---|
| trace | false | true собирает passed и failed entries. Объект TraceOptions позволяет выбрать режим и лимиты. |
| timing | false | Добавляет durationMs и раздельное поле timing. |
| output | false | После завершения печатает formatComparison(result) через logger. Автоматически включает failure trace; для полного отключения используйте trace: { mode: "none" }. |
| colors | "auto" | ANSI-цвета для автоматического output: auto, always, never. |
| logger | console.log | Получает одну готовую строку отчёта. Не вызывается без output: true. |
| redact | отсутствует | Callback возвращает true, если значение данной стороны и пути нельзя сохранять в failure trace. Работает fail-closed. |
TraceOptions:
| Поле | По умолчанию | Описание |
|---|---|---|
| mode | failure trace | none, summary, failures или full. |
| includePassed | зависит от mode | Явно включает или исключает успешные записи. При trace: true и mode: "full" включено. |
| maxEntries | 10_000 | Максимум сохранённых entries. Для mode: "summary" default равен 50. |
| maxValueLength | 200 | Максимальная длина сохранённого preview/строки. |
| maxPathDepth | 64 | Максимум сегментов пути; при усечении сохраняются начало и конец. |
| maxPathLength | 1_024 | Максимальная длина форматированной строки пути. |
| retainValues | false | Хранит raw references в actual/expected. Может удерживать весь входной граф в памяти и раскрывать секреты. |
Поведение mode:
| Mode | Что сохраняется |
|---|---|
| none | Trace полностью отключён, даже если включён output. |
| summary | Только failures, по умолчанию максимум 50 записей. |
| failures | Только failures, общий default maxEntries. |
| full | Passed, failed и info entries. |
Collector limit trace.maxEntries и renderer limit formatComparison(..., { maxEntries }) — разные вещи. Первый ограничивает память во время сравнения и выставляет traceTruncated; второй только скрывает часть уже собранных entries при отображении.
Защитные лимиты
| Параметр | По умолчанию | Что ограничивает | Статус при остановке |
|---|---:|---|---|
| maxDepth | 512 | Глубину пути сравнения. | INDETERMINATE |
| maxNodes | 1_000_000 | Количество comparison nodes, включая внутренние probes. | INDETERMINATE |
| maxOperations | Infinity | Общую работу: nodes, properties, collection probes и binary bytes. | INDETERMINATE |
| timeoutMs | Infinity | Кооперативное wall-clock время. | INDETERMINATE |
| signal | отсутствует | Кооперативную отмену между операциями. | ABORTED |
| maxDifferences | Infinity | Количество доказанных различий, после которого обход прекращается. | NOT_EQUAL с differencesTruncated: true |
| maxCollectionComparisons | 100_000 | Число кандидатов backtracking для object-heavy Map/Set. | INDETERMINATE |
Исчерпание ресурса никогда не маскируется под NOT_EQUAL. Лимит не доказывает неравенство.
timeoutMs и AbortSignal кооперативны: они проверяются между операциями. Синхронный API не может прервать Proxy trap или callback, который никогда не возвращает управление.
Числовые лимиты принимают неотрицательные значения и Infinity; дробные значения округляются вниз. NaN, отрицательное число и иное невалидное значение заменяются соответствующим значением по умолчанию.
Поддерживаемые типы
| Тип | Семантика по умолчанию |
|---|---|
| Примитивы | SameValue (Object.is). |
| Обычные и null-prototype объекты | Own string/symbol/non-enumerable descriptors; порядок ключей не важен. |
| Экземпляры классов | По умолчанию fail-closed INDETERMINATE; используйте custom comparator или явно задайте classInstanceMode: "properties". |
| Массивы | Порядок и длина важны; hole отличается от undefined; custom properties тоже сравниваются. |
| arguments | Сравнение own properties как структурного объекта. |
| Map | Порядок вставки не важен; keys и values сопоставляются структурно. |
| Set | Порядок вставки не важен; значения сопоставляются структурно. |
| Циклы и shared references | Поддерживаются; в graph mode проверяется полная topology. |
| Date | getTime() по SameValue, включая две invalid dates. |
| RegExp | Source, flags, lastIndex и custom properties. |
| ArrayBuffer, SharedArrayBuffer | Bytes, fixed/resizable или fixed/growable state, maxByteLength и custom properties в exact mode. |
| DataView, typed arrays | Brand, length, bytes, exact/view политика backing store и fixed-length/length-tracking state, когда его можно безопасно наблюдать. |
| Node.js Buffer | Typed-array семантика; для сравнения только видимого содержимого обычно нужен binaryMode: "view". |
| Error и subclasses | name, message, cause, AggregateError.errors, custom properties; stack opt-in. |
| Boxed Number, String, Boolean, BigInt, Symbol | Unboxed value и custom properties. |
| URL | Нормализованный href и custom properties. |
| URLSearchParams | Строковая сериализация с учётом порядка и custom properties. |
| Functions | Непрозрачны: равны только по reference; source и own properties не сравниваются. |
| Promise, WeakMap, WeakSet, WeakRef, FinalizationRegistry | Непрозрачны: равны только по reference. |
| Неизвестные opaque built-ins/host objects | INDETERMINATE с UNSUPPORTED_TYPE; добавьте custom comparator. |
Обычные accessors не вызываются. Сравниваются data/accessor вид и references getter/setter. Исключение — opt-in compareErrorStack: true, поскольку чтение stack необходимо для его сравнения.
Графы, циклы и aliasing
Default equalityMode: "graph" сравнивает не только значения, но и схему переиспользования ссылок.
const shared = { value: 1 };
const left = {
first: shared,
second: shared,
};
const right = {
first: { value: 1 },
second: { value: 1 },
};
deepEqual(left, right); // false: topology различается
deepEqual(left, right, { equalityMode: "structural" }); // trueОба режима поддерживают циклы:
const left: { self?: unknown } = {};
const right: { self?: unknown } = {};
left.self = left;
right.self = right;
deepEqual(left, right); // trueВ graph mode используется двустороннее отображение left → right и right → left. Structural mode memoizes конкретные пары (left, right), не требуя одинаковой alias-кратности.
Map и Set
Глубокие ключи Map
const left = new Map([[{ id: 1 }, { role: "admin" }]]);
const right = new Map([[{ id: 1 }, { role: "admin" }]]);
deepEqual(left, right); // true
deepEqual(left, right, { mapKeyMode: "identity" }); // falsemapKeyMode: "identity" соответствует native key identity/SameValueZero. Values при этом всё равно сравниваются глубоко.
Неупорядоченные Set
deepEqual(
new Set([{ id: 1 }, { id: 2 }]),
new Set([{ id: 2 }, { id: 1 }]),
); // trueПримитивные коллекции используют линейный нерекурсивный путь. Object-heavy коллекции сначала используют итеративный positional fast path, затем при необходимости итеративный bijective backtracking; размер коллекции больше не расходует JavaScript call stack. Неоднозначное сопоставление всё ещё может иметь факториальную сложность. Всегда задавайте maxCollectionComparisons для недоверенных данных.
Бинарные данные
exact и view
const leftBuffer = Uint8Array.from([9, 1, 2]).buffer;
const rightBuffer = Uint8Array.from([1, 2, 9]).buffer;
const leftView = new Uint8Array(leftBuffer, 1, 2);
const rightView = new Uint8Array(rightBuffer, 0, 2);
deepEqual(leftView, rightView); // false: offset и backing buffer различаются
deepEqual(leftView, rightView, { binaryMode: "view" }); // trueexact предназначен для сравнения полного состояния памяти и topology backing buffers. Он учитывает fixed/resizable/growable state, maxByteLength и fixed-length/length-tracking state RAB views. Также учитывается byte representation: например, разные payload bits двух NaN могут различаться.
Копии growable SharedArrayBuffer разделяют один backing store. Если геометрия двух GSAB views совместима и с fixed-length, и с length-tracking state, JavaScript не даёт безопасного немутирующего способа различить этот internal slot. Exact mode возвращает INDETERMINATE, а не ложный EQUAL.
view предназначен для семантики «одинаковые видимые байты». Brand и длина view всё равно проверяются. Для Node.js Buffer этот режим обычно соответствует ожидаемому сравнению содержимого, потому что два Buffer могут использовать разные offsets общего slab.
У typed arrays продолжают сравниваться custom string/symbol/non-enumerable properties. Поэтому большие views требуют перечисления own keys даже после быстрого бинарного сравнения.
Дескрипторы, getters и прототипы
Getters не исполняются
let reads = 0;
const getter = () => {
reads += 1;
return 42;
};
const left = {};
const right = {};
Object.defineProperty(left, "value", { enumerable: true, get: getter });
Object.defineProperty(right, "value", { enumerable: true, get: getter });
deepEqual(left, right); // true
reads; // 0Два accessor properties равны, когда используют те же getter и setter references. Data property и accessor никогда не считаются одинаковыми.
Descriptor flags
const left = {};
const right = {};
Object.defineProperty(left, "id", { value: 1, writable: false });
Object.defineProperty(right, "id", { value: 1, writable: true });
deepEqual(left, right); // true: значения равны
deepEqual(left, right, { comparePropertyDescriptors: true }); // falseПрототипы и классы
class UserA {
constructor(readonly id: number) {}
}
class UserB {
constructor(readonly id: number) {}
}
deepCompare(new UserA(1), new UserA(1)).status; // "INDETERMINATE"
deepEqual(new UserA(1), new UserA(1), {
classInstanceMode: "properties",
}); // true: явная политика observable state
deepEqual(new UserA(1), new UserB(1), {
strictPrototypes: false,
classInstanceMode: "properties",
}); // true: явная shape-only политикаJavaScript reflection не может прочитать #private fields, closure state, Proxy handler state или произвольное WeakMap-backed состояние домена. Поэтому default classInstanceMode: "opaque" не доказывает равенство разных экземпляров пользовательского класса. Для доменных классов предпочтителен custom comparator. classInstanceMode: "properties" — явный opt-in к observable own-property семантике; одновременное отключение strictPrototypes является ещё более сильным shape-only opt-in.
Трейс, пути и форматирование
TraceEntry
| Поле | Значение |
|---|---|
| path | Готовый стабильный путь от $. |
| pathSegments | Структурированные сегменты для программной обработки. |
| depth | Глубина записи. |
| status | passed, failed или info. |
| code | Машиночитаемый TraceCode. Используйте его вместо разбора message. |
| message | Человекочитаемое описание на английском. |
| actualType, expectedType | Безопасно определённые типы сторон. |
| actualPreview, expectedPreview | Ограниченные строковые previews. |
| actual, expected | Bounded snapshots; raw references только с retainValues: true. |
| metadata | Дополнительные структурированные сведения, например byte index. |
| operation | Reflection/callback operation, выбросившая исключение. |
| pathTruncated | Путь был ограничен настройками trace. |
Примеры путей:
| Значение | Путь |
|---|---|
| Обычное поле | $.user.name |
| Необычный string key | $["content-type"] |
| Array index | $.items[3] |
| Symbol key | $[Symbol("token")] |
| Map key/value | $.cache.<map:0>.key, $.cache.<map:0>.value |
| Set item | $.roles.<set:1> |
| Внутреннее поле | $.bytes.<byteLength> |
| Binary mismatch | $.payload[byte:32767] |
Полный union TraceCode:
SAME_REFERENCE
PRIMITIVE_EQUAL, PRIMITIVE_MISMATCH, TYPE_MISMATCH, TAG_MISMATCH
PROTOTYPE_MISMATCH, GRAPH_TOPOLOGY_MATCH, GRAPH_TOPOLOGY_MISMATCH
PROPERTY_MATCH, PROPERTY_MISMATCH, PROPERTY_MISSING, PROPERTY_UNEXPECTED
DESCRIPTOR_MISMATCH, OBJECT_MATCH, OBJECT_INTEGRITY_MISMATCH
ARRAY_MATCH, ARRAY_LENGTH_MISMATCH, ARRAY_HOLE_MISMATCH
DATE_MATCH, DATE_MISMATCH, REGEXP_MATCH, REGEXP_MISMATCH
MAP_MATCH, MAP_SIZE_MISMATCH, MAP_ENTRY_MISSING
SET_MATCH, SET_SIZE_MISMATCH, SET_VALUE_MISSING
BINARY_MATCH, BINARY_MISMATCH
ERROR_MATCH, ERROR_MISMATCH, URL_MATCH, URL_MISMATCH
BOXED_VALUE_MATCH, BOXED_VALUE_MISMATCH, OPAQUE_REFERENCE_MISMATCH
CUSTOM_COMPARATOR_MATCH, CUSTOM_COMPARATOR_MISMATCH, CUSTOM_COMPARATOR_ERROR
INSPECTION_ERROR, UNSUPPORTED_TYPE, ABORTED
MAX_DEPTH_EXCEEDED, COMPARISON_LIMIT_REACHEDТексты message могут уточняться между версиями. Для интеграций используйте status, code, path и pathSegments.
Redaction
const result = deepCompare(actual, expected, {
trace: { mode: "failures", maxValueLength: 80 },
redact: ({ path, value }) =>
/password|token|secret/i.test(path) || value instanceof Uint8Array,
});Callback вызывается отдельно для actual и expected. Если он выбрасывает исключение, значение скрывается — политика fail-closed. Redaction также применяется к диагностике исключений и terminal reason.
По умолчанию trace не удерживает raw object graph. retainValues: true используйте только в доверенной локальной диагностике.
formatComparison()
RenderOptions:
| Поле | По умолчанию | Описание |
|---|---|---|
| colors | "auto" | auto, always, never. auto учитывает TTY и NO_COLOR. |
| includePassed | true | Показывать passed entries, если они были собраны. |
| maxEntries | 200 | Renderer limit; не изменяет исходный result. |
| maxValueLength | 140 | Ограничение отображаемого значения. |
Пример отчёта:
✗ DEEP COMPARISON FAILED
Time: 0.418 ms
Breakdown: 0.351 ms compare + 0.067 ms trace
Work: 7 nodes · 1 differences · 0 repeated/circular refs · 0 collection candidates
Trace
✗ FAILED $.user.role [PRIMITIVE_MISMATCH] — Primitive values differ.
actual "editor"
expected "admin"Форматирование и вызов logger выполняются после core timing и не входят в durationMs.
Структура результата
CompareResult
| Поле | Тип | Описание |
|---|---|---|
| status | ComparisonStatus | Один из пяти исходов. |
| equal | boolean | true только для EQUAL. |
| aborted | boolean | true только для ABORTED. |
| reason? | string | Причина INDETERMINATE, ABORTED или ERROR. |
| abortReason? | string | Deprecated alias поля reason. |
| differencesTruncated | boolean | Обход остановлен лимитом различий. |
| traceTruncated | boolean | Collector отбросил entries после trace.maxEntries. |
| trace | readonly TraceEntry[] | Immutable structured trace. |
| stats | ComparisonStats | Счётчики выполненной работы. |
| durationMs? | number | Полное core-время; только при timing: true. |
| timing? | ComparisonTiming | Раздельные времена; только при timing: true. |
Result, trace array, stats и timing замораживаются перед возвратом.
ComparisonTiming
| Поле | Описание |
|---|---|
| comparisonMs | Traversal/equality без измеренного построения trace. |
| traceConstructionMs | Path, preview, redaction и создание trace entries. |
| totalMs | Полная core operation; равно durationMs. |
ComparisonStats
| Поле | Описание |
|---|---|
| nodesCompared | Comparison nodes, включая внутренние candidate probes. |
| operations | Общий work counter для resource budget. |
| propertiesCompared | Проверенные own properties. |
| passedNodes | Успешные nodes финального пути; speculative probes исключены. |
| failedNodes | Неуспешные nodes финального пути; speculative probes исключены. |
| differences | Доказанные пользовательские различия. |
| circularReferences | Замкнутые повторные или циклические ссылки. |
| collectionCandidateComparisons | Все проверенные кандидаты unordered collections. |
| mapCandidateComparisons | Кандидаты Map. |
| setCandidateComparisons | Кандидаты Set. |
| memoizationHits | Повторно найденные сравниваемые пары. |
| deepestLevel | Максимальная достигнутая глубина. |
| traceEntriesDropped | Entries, отброшенные collector limit. |
| traceEntries | Фактически сохранённые entries. |
Custom comparators
Custom comparator полезен для decimal types, геометрии, ORM entities, immutable collections и host objects с доменной семантикой.
import type { CustomComparator } from "deep-object-compare-trace";
class ApproximateNumber {
constructor(readonly value: number) {}
}
const approximateComparator: CustomComparator = {
name: "approximate-number",
canCompare: (actual, expected) =>
actual instanceof ApproximateNumber &&
expected instanceof ApproximateNumber,
compare: (actual, expected) => {
const left = actual as ApproximateNumber;
const right = expected as ApproximateNumber;
const delta = Math.abs(left.value - right.value);
return {
equal: delta <= 0.01,
reason: `Absolute delta ${delta} must be <= 0.01.`,
};
},
};
deepEqual(
new ApproximateNumber(1),
new ApproximateNumber(1.005),
{ customComparators: [approximateComparator] },
); // trueКонтракт:
canCompare()должен возвращатьtrueтолько для полностью принадлежащей компаратору пары;- первый совпавший comparator останавливает dispatch;
compare()возвращает boolean или{ equal, reason? };- callbacks синхронны; Promise не поддерживается;
- исключения, неверный return value и recursive re-entry преобразуются в
ERRORсCUSTOM_COMPARATOR_ERROR; - comparator не должен повторно вызывать сравнение с самим собой в
customComparators.
Практические рецепты
Production-профиль для недоверенных данных
const result = deepCompare(actual, expected, {
executionMode: "first-difference",
trace: {
mode: "failures",
maxEntries: 20,
maxValueLength: 120,
maxPathDepth: 48,
maxPathLength: 512,
},
maxDepth: 256,
maxNodes: 100_000,
maxOperations: 2_000_000,
maxCollectionComparisons: 20_000,
timeoutMs: 100,
redact: ({ path }) => /password|token|secret/i.test(path),
});Подбирайте limits по своим данным и latency budget. Пример не является универсальной безопасной константой.
Собрать несколько различий
const result = deepCompare(actual, expected, {
executionMode: "all-differences",
maxDifferences: 25,
trace: { mode: "failures", maxEntries: 25 },
});
if (result.differencesTruncated) {
console.warn("Показаны не все различия");
}Записать отчёт своим logger
deepCompare(actual, expected, {
output: true,
colors: "never",
logger: (message) => applicationLogger.error({ message }),
});Предварительно отменённая операция
const controller = new AbortController();
controller.abort("request closed");
const result = deepCompare(actual, expected, {
signal: controller.signal,
trace: { mode: "failures" },
});
result.status; // "ABORTED"Поскольку API синхронный, timer в том же event loop не сможет сработать посреди выполняющегося сравнения. Для жёсткого внешнего deadline запускайте сравнение в Worker и завершайте Worker; timeoutMs остаётся кооперативной внутренней защитой.
Сравнить API payload без служебных полей
Библиотека намеренно не содержит ignore-path DSL. Явно нормализуйте данные перед сравнением — так правило остаётся типизируемым и проверяемым.
type ApiResponse = {
requestId: string;
receivedAt: string;
[key: string]: unknown;
};
const normalize = ({
requestId: _requestId,
receivedAt: _receivedAt,
...value
}: ApiResponse) => value;
deepEqual(normalize(actualResponse), normalize(expectedResponse));Использование в тестах
import { assertDeepEqual } from "deep-object-compare-trace";
test("response matches contract", () => {
assertDeepEqual(actualResponse, expectedResponse, {
trace: { mode: "failures" },
});
});Производительность и сложность
- Используйте
deepEqual()для hot path, если нужен только boolean. - Не включайте
trace,timing, object integrity и descriptor flags без необходимости. first-differenceдешевле полного diff при раннем mismatch.- Обычные object/array обходы линейны относительно проверенных properties.
- Примитивные
Map/Setиспользуют специализированный линейный путь. - Неоднозначные object-heavy
Map/Setмогут потребовать факториальный backtracking; его ограничиваетmaxCollectionComparisons. - Обычные глубокие object graphs обходятся собственным стеком движка. Тестируется цепочка глубиной 100 000, но для неё необходимо явно поднять
maxDepth. - Exact typed-array mode сравнивает backing store; view mode обычно дешевле для slices и Buffer.
- Строгая descriptor-safe семантика массивов дороже прямого цикла по значениям, поскольку должна различать holes, accessors и custom properties.
На тестовой машине библиотека после оптимизаций ускорилась относительно собственного первого baseline до 3,7 раза на примитивных Set и до 2,7 раза на typed arrays. fast-deep-equal остаётся быстрее на простых ациклических данных, но не предоставляет эквивалентные graph, descriptor, trace и resource-control гарантии.
Полная воспроизводимая методика и честные цифры: сравнение с fast-deep-equal.
Безопасность и ограничения
Гарантии
- Входные графы не мутируются библиотекой.
- Ordinary getters/setters не вызываются ради сравнения.
- Reflection и custom-comparator exceptions преобразуются в
ERROR, а не выходят наружу из core comparison. - Revoked/hostile Proxy получает контролируемый результат с путём и названием упавшей операции.
- Trace по умолчанию хранит bounded previews, а не raw object references.
- Redactor действует fail-closed.
- Resource exhaustion возвращает
INDETERMINATE, а не ложное неравенство.
logger относится к внешнему output-слою. Если пользовательский logger при output: true выбросит исключение, оно будет передано вызывающему коду после уже завершённого сравнения.
Честные ограничения
- Синхронный код не способен прервать Proxy trap или callback, который не возвращает управление.
timeoutMsиmaxOperationsпроверяются до и между work units движка. Один native reflection step, напримерReflect.ownKeys, атомарен и не может быть остановлен посередине; для жёсткого внешнего deadline используйте Worker.- Proxy traps неизбежно исполняются самим JavaScript reflection API; гарантия «не вызывать getter» не означает «не вызывать Proxy traps».
- Identity Proxy handler и замкнутое в нём состояние недоступны reflection. Движок сравнивает поведение и наблюдаемую поверхность во время вызова, а не скрытый handler object.
- Concurrent mutation входов не поддерживает транзакционный snapshot. Особенно это важно для
SharedArrayBuffer. - Functions сравниваются только по identity, без source и own properties.
- Async comparators, streaming trace и worker traversal не входят в синхронный core.
- Неизвестные browser/Node host objects без надёжной наблюдаемой семантики возвращают
INDETERMINATE. - Разные экземпляры пользовательских классов по умолчанию возвращают
INDETERMINATE, поскольку private/internal state ненаблюдаем.classInstanceMode: "properties"осознанно игнорирует это состояние. - Graph equality — выбранная строгая политика, а не единственно возможное философское определение equality. Для tree-like semantics используйте
equalityMode: "structural". trace.retainValues: trueможет удерживать большой или чувствительный граф в памяти.- Большие arrays и typed arrays платят за проверку descriptors/custom own properties; это осознанная цена точности.
Экспортируемые типы
Пакет экспортирует:
import type {
BinaryMode,
ClassInstanceMode,
ColorMode,
CompareOptions,
CompareResult,
ComparisonStats,
ComparisonStatus,
ComparisonTiming,
CustomComparator,
CustomComparisonResult,
EqualityMode,
ExecutionMode,
MapKeyMode,
PathSegment,
PrimitiveMode,
RedactionContext,
RenderOptions,
TraceCode,
TraceEntry,
TraceMode,
TraceOptions,
TraceStatus,
} from "deep-object-compare-trace";Разработка
npm test # correctness suite
npm run test:watch # watch mode
npm run check # TypeScript --noEmit
npm run typecheck # alias для check
npm run lint # ESLint
npm run coverage # V8 coverage
npm run build # ESM, CJS, declarations, source maps
npm run test:package # smoke-test упакованного ESM/CommonJS/TypeScript consumer
npm run verify # полный prepublish gate
npm run bench # внутренние benchmarksCoverage gate требует минимум 85% statements/lines, 80% branches и 98% functions. Текущее покрытие renderer превышает 96% lines.
npm pack пересобирает dist через prepack. npm publish дополнительно запускает полный verify через prepublishOnly до упаковки.
GitLab CI запускает тот же verify на Node.js 22, 24 и 26.
Текущий correctness gate содержит 139 полностью собственных тестов: циклы, alias topology, ambiguous Map/Set, binary views, descriptors, getter bombs, hostile и revoked proxies, cross-realm значения, monkey-patched intrinsics, seeded fuzz, строки до 10 MiB, object width 100 000 и object depth 100 000.
Дополнительные документы:
