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

object-graph-delta

v1.0.0

Published

> Structural diffs for object graphs with cycle safety and configurable array semantics.

Readme

object-graph-delta

Structural diffs for object graphs with cycle safety and configurable array semantics.

object-graph-delta computes deterministic CREATE / CHANGE / REMOVE events between two object/array graphs. It is designed for scenarios where you need path-based change events instead of just boolean equality.

Features

  • Deterministic diff events: Emits REMOVE and CHANGE first, then CREATE, in stable key/index order.
  • Operational array policies: Choose between diff, atomic, and ignore array handling.
  • Cycle-safe traversal: Optional pair-based circular reference tracking to prevent infinite recursion.
  • Rich-type equality: Correct value comparison for Date, RegExp, and boxed primitives.
  • Object key filtering: Skip specific object keys via keysToSkip without affecting array indices.
  • TypeScript-friendly API: Strongly typed paths and diff result unions.

Why Use It?

| Need | What this package gives you | | --- | --- | | Patch-like change events | CREATE / CHANGE / REMOVE with explicit paths | | Fine control over array behavior | Index diffing, atomic compare, or full ignore | | Safe comparison of cyclic graphs | Built-in cycle tracking (trackCircularReferences) | | Correct treatment of tricky JS values | Object.is semantics, rich-type handling, boxed primitive equality |

Diff Model

Each diff entry includes a path ((string | number)[]) and one of:

  • CREATE: { type: 'CREATE', path, value }
  • REMOVE: { type: 'REMOVE', path, oldValue }
  • CHANGE: { type: 'CHANGE', path, value, oldValue }

Example path:

  • ['users', 0, 'name'] means root.users[0].name

Installation

npm install object-graph-delta
# or
pnpm add object-graph-delta
# or
yarn add object-graph-delta

Quick Start

import { diff } from 'object-graph-delta';

const previous = { a: 1, b: 2 };
const current = { b: 3, c: 4 };

const changes = diff(previous, current);
console.log(changes);

Output:

[
  { type: 'REMOVE', path: ['a'], oldValue: 1 },
  { type: 'CHANGE', path: ['b'], value: 3, oldValue: 2 },
  { type: 'CREATE', path: ['c'], value: 4 }
]

Operational Policies

Array handling is the core runtime policy switch.

Array Policy Matrix

| arrayPolicy | Behavior | Emits | | --- | --- | --- | | 'diff' | Traverses arrays by index recursively | Per-index CREATE / CHANGE / REMOVE | | 'atomic' | Treats each array as one leaf value | At most one CHANGE at the array path | | 'ignore' | Skips array comparison entirely | Nothing for arrays |

Atomic Equality Modes

Used only when arrayPolicy: 'atomic':

| arrayEquality | Equality rule | | --- | --- | | 'reference' | Equal only if a === b | | 'shallow' | Equal if same length and each element matches via Object.is |

Default Policy

Default options are:

{
  trackCircularReferences: true,
  arrayPolicy: 'diff',
  keysToSkip: []
}

When arrayPolicy is 'atomic' and arrayEquality is omitted, 'shallow' is used.

arrayEquality is only relevant in atomic mode. Set it explicitly when using arrayPolicy: 'atomic'.

Examples (Simple to Complex)

1. Basic Value Change

import { diff } from 'object-graph-delta';

diff({ a: 1 }, { a: 2 });
// [{ type: 'CHANGE', path: ['a'], value: 2, oldValue: 1 }]

2. Nested Array Diff (Default arrayPolicy: 'diff')

import { diff } from 'object-graph-delta';

diff({ list: [1, 2, 3] }, { list: [1, 3] });
// [
//   { type: 'CHANGE', path: ['list', 1], value: 3, oldValue: 2 },
//   { type: 'REMOVE', path: ['list', 2], oldValue: 3 }
// ]

3. Atomic Arrays (reference vs shallow)

import { diff } from 'object-graph-delta';

const previous = { a: [1, 2] };
const current = { a: [1, 2] };

diff(previous, current, { arrayPolicy: 'atomic', arrayEquality: 'reference' });
// [{ type: 'CHANGE', path: ['a'], value: [1, 2], oldValue: [1, 2] }]

diff(previous, current, { arrayPolicy: 'atomic', arrayEquality: 'shallow' });
// []

4. Ignore Arrays Entirely

import { diff } from 'object-graph-delta';

diff(
  { items: [1, 2, 3], version: 1 },
  { items: [9, 8, 7], version: 2 },
  { arrayPolicy: 'ignore' }
);
// [{ type: 'CHANGE', path: ['version'], value: 2, oldValue: 1 }]

5. Skip Keys and Handle Cycles

import { diff } from 'object-graph-delta';

const previous: Record<string, unknown> = { a: 1, meta: { changed: false } };
previous.self = previous;

const current: Record<string, unknown> = { a: 2, meta: { changed: true } };
current.self = current;

diff(previous, current, {
  trackCircularReferences: true,
  keysToSkip: ['meta']
});
// [{ type: 'CHANGE', path: ['a'], value: 2, oldValue: 1 }]

How to Use

Step 1: Provide Two Root Containers

The public API compares two root containers (objects or arrays):

import { diff } from 'object-graph-delta';

const previous = { user: { name: 'Alice' } };
const current = { user: { name: 'Bob' } };

Step 2: Choose Options for Your Use Case

const result = diff(previous, current, {
  arrayPolicy: 'diff',
  trackCircularReferences: true,
  keysToSkip: ['_internal']
});

Step 3: Consume Diff Events

for (const change of result) {
  if (change.type === 'CHANGE') {
    // change.path, change.oldValue, change.value
  }
}

API Reference

Function

diff<V>(
  previous: Container<V>,
  current: Container<V>,
  options?: DiffOptions
): DiffResult<Node<V>>[]

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | trackCircularReferences | boolean | true | Prevents infinite recursion by tracking compared container pairs on the current traversal path | | arrayPolicy | 'diff' \| 'atomic' \| 'ignore' | 'diff' | Controls array handling mode | | arrayEquality | 'reference' \| 'shallow' | 'shallow' (when arrayPolicy: 'atomic' and omitted) | Used in atomic mode to decide array equality | | keysToSkip | readonly string[] | [] | Skips object keys (does not apply to array indices) |

Behavioral Notes (Backed by Tests)

  • Equality is based on Object.is for leaf values.
  • NaN equals NaN; +0 and -0 are different.
  • Date values are compared by timestamp.
  • RegExp values are compared by string form (/pattern/flags).
  • Boxed primitives (new Number, new String, new Boolean, Object(1n)) compare by underlying value.
  • Symbol keys and non-enumerable keys are ignored (iteration is based on Object.keys).
  • Inherited keys can affect presence/value checks because key existence uses the in operator.
  • Alias paths in cyclic structures can yield multiple diffs for the same logical value change at different paths.