npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deep-object-compare-trace

v0.1.0

Published

Dependency-free deep equality for complex JavaScript values with explainable traces and timing.

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.js Buffer;
  • безопасное сравнение 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.

Содержание

Установка

npm install deep-object-compare-trace
pnpm add deep-object-compare-trace
yarn 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" }); // false

mapKeyMode: "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" }); // true

exact предназначен для сравнения полного состояния памяти и 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                   # внутренние benchmarks

Coverage 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.

Дополнительные документы:

Лицензия

MIT