πŸš€ The Fast, Accurate, JavaScript Objects Diffing & Patching Library

thangaganapathy

Thanga Ganapathy

Posted on May 1, 2024

πŸš€ The Fast, Accurate, JavaScript Objects Diffing & Patching Library

Welcome,

I am here to introduce the new JavaScript library for objects diffing & patching.

@opentf/obj-diff

Live Demo with Visualization

Features

  • Deep Objects Diffing

  • Patching

  • Supports comparing custom object types (via diffWith)

  • TypeScript Support

  • Cross-Platform

Supported Types

  • Primitives

    • Undefined
    • Null
    • Number
    • String
    • Boolean
    • BigInt
  • Objects

    • Plain Objects, e.g. {}
    • Array
    • Date
    • Map
    • Set

Let's begin with some basics, and we will go through our examples to see the existing solutions and their issues.

Diffing

The diffing is the method used to compare objects, change detection or objects tracking.

The diff result is called patches.

It allows us to send fewer data to the backend to apply the patches.

Patching

The patching is the method used to re-create the modified object at the other end using the original object + Patches (the diff result).

Accuracy

Here, we are going to find out how accurate our library is, and we need similar, popular libraries to compare against them.

So, let's pick three popular libraries.

  1. Microdiff

  2. just-diff

  3. deep-object-diff

Let us first create a test file.

import { diff } from '@opentf/obj-diff';
import mDiff from "microdiff";
import { diff as jDiff } from "just-diff";
import { detailedDiff } from "deep-object-diff";

function run(a, b) {
  try {
    console.log("Micro Diff:");
    console.log(mDiff(a, b));
    console.log();
  } catch (error) {
    console.log("Error: ", error.message);
  }

  try {
    console.log("Just Diff:");
    console.log(jDiff(a, b));
    console.log();
  } catch (error) {
    console.log("Error: ", error.message);
  }

  try {
    console.log("deep-object-diff:");
    console.log(detailedDiff(a, b));
    console.log();
  } catch (error) {
    console.log("Error: ", error.message);
  }

  try {
    console.log("diff:");
    console.log(diff(a, b));
    console.log();
  } catch (error) {
    console.log("Error: ", error.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's start our complete testing.

1. Simple with no difference between objects

run({}, {});
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[]

Just Diff:
[]

deep-object-diff:
{
  added: {},
  deleted: {},
  updated: {},
}

diff:
[]
Enter fullscreen mode Exit fullscreen mode

2. Simple with single difference

run({ a: 1 }, { a: 2 });
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[
  {
    path: [ "a" ],
    type: "CHANGE",
    value: 2,
    oldValue: 1,
  }
]

Just Diff:
[
  {
    op: "replace",
    path: [ "a" ],
    value: 2,
  }
]

deep-object-diff:
{
  added: {},
  deleted: {},
  updated: {
    a: 2,
  },
}

diff:
[
  {
    t: 2,
    p: [ "a" ],
    v: 2,
  }
]
Enter fullscreen mode Exit fullscreen mode

3. Primitives

const a = [undefined, null, "string", 0, 1, 1n, true, false];
const b = [undefined, null, "string", 0, 1, 1n, true, false];
run(a, b);
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[]

Just Diff:
[]

deep-object-diff:
{
  added: {},
  deleted: {},
  updated: {},
}

diff:
[]
Enter fullscreen mode Exit fullscreen mode

All Working fine, no issues.

4. Number properties

const a = [NaN, Infinity, -Infinity];
const b = [NaN, Infinity, Infinity];
run(a, b);
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[
  {
    path: [ 2 ],
    type: "CHANGE",
    value: Infinity,
    oldValue: -Infinity,
  }
]

Just Diff:
[
  {
    op: "replace",
    path: [ 0 ],
    value: NaN,
  }, {
    op: "replace",
    path: [ 2 ],
    value: Infinity,
  }
]

deep-object-diff:
{
  added: {},
  deleted: {},
  updated: {
    "0": NaN,
    "2": Infinity,
  },
}

diff:
[
  {
    t: 2,
    p: [ 2 ],
    v: Infinity,
  }
]
Enter fullscreen mode Exit fullscreen mode

As you can see the just-diff & deep-object-diff incorrectly reporting the NaN value changed.

5. Simple deep objects

const a = {
  a: {
    b: {
      c: [1, 2, 3]
    },
    d: null
  },
  text: 'Hello'
}

const b = {
  a: {
    b: {
      c: [1, 2, 3, 4, 5]
    },
  },
  text: 'Hello World'
}

run(a, b);
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[
  {
    type: "CREATE",
    path: [ "a", "b", "c", 3 ],
    value: 4,
  }, {
    type: "CREATE",
    path: [ "a", "b", "c", 4 ],
    value: 5,
  }, {
    type: "REMOVE",
    path: [ "a", "d" ],
    oldValue: null,
  }, {
    path: [ "text" ],
    type: "CHANGE",
    value: "Hello World",
    oldValue: "Hello",
  }
]

Just Diff:
[
  {
    op: "remove",
    path: [ "a", "d" ],
  }, {
    op: "replace",
    path: [ "text" ],
    value: "Hello World",
  }, {
    op: "add",
    path: [ "a", "b", "c", 3 ],
    value: 4,
  }, {
    op: "add",
    path: [ "a", "b", "c", 4 ],
    value: 5,
  }
]

deep-object-diff:
{
  added: {
    a: {
      b: {
        c: { '3': 4, '4': 5 }
      },
    },
  },
  deleted: {
    a: {
      d: undefined,
    },
  },
  updated: {
    text: "Hello World",
  },
}

diff:
[
  {
    t: 1,
    p: [ "a", "b", "c", 3 ],
    v: 4,
  }, {
    t: 1,
    p: [ "a", "b", "c", 4 ],
    v: 5,
  }, {
    t: 0,
    p: [ "a", "d" ],
  }, {
    t: 2,
    p: [ "text" ],
    v: "Hello World",
  }
]
Enter fullscreen mode Exit fullscreen mode

All Working fine, no issues.

6. Different object types

const a = {
  a: 1,
  b: 2,
  c: 3
}
const b = [1, 2, 3]

run(a, b);
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[
  {
    type: "REMOVE",
    path: [ "a" ],
    oldValue: 1,
  }, {
    type: "REMOVE",
    path: [ "b" ],
    oldValue: 2,
  }, {
    type: "REMOVE",
    path: [ "c" ],
    oldValue: 3,
  }, {
    type: "CREATE",
    path: [ 0 ],
    value: 1,
  }, {
    type: "CREATE",
    path: [ 1 ],
    value: 2,
  }, {
    type: "CREATE",
    path: [ 2 ],
    value: 3,
  }
]

Just Diff:
[
  {
    op: "remove",
    path: [ "c" ],
  }, {
    op: "remove",
    path: [ "b" ],
  }, {
    op: "remove",
    path: [ "a" ],
  }, {
    op: "add",
    path: [ 0 ],
    value: 1,
  }, {
    op: "add",
    path: [ 1 ],
    value: 2,
  }, {
    op: "add",
    path: [ 2 ],
    value: 3,
  }
]

deep-object-diff:
{
  added: {
    "0": 1,
    "1": 2,
    "2": 3,
  },
  deleted: {
    a: undefined,
    b: undefined,
    c: undefined,
  },
  updated: {},
}

diff:
[
  {
    t: 2,
    p: [],
    v: [ 1, 2, 3 ],
  }
]
Enter fullscreen mode Exit fullscreen mode

As you can see, the other libraries are reporting changes within the original object, but the actual object type was changed from plain object to Array.

Note: The empty path array { p: [] } in our diff result denotes the Root path.

7. Passing null as object

const a = {
  a: 1,
  b: 2,
  c: 3
}
const b = null

run(a, b);
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
Error:  newObj is not an Object. (evaluating 'key in newObj')
Just Diff:
Error:  both arguments must be objects or arrays
deep-object-diff:
{
  added: {},
  deleted: {},
  updated: null,
}

diff:
[
  {
    t: 2,
    p: [],
    v: null,
  }
]
Enter fullscreen mode Exit fullscreen mode

8. Commonly used object types: Maps & Sets

const a = {
  obj: {
    m: new Map([['x', 0], ['y', 1]]),
    s: new Set([1, 2, 3])
  }
}

const b = {
  obj: {
    m: new Map([['x', 1], ['y', 0]]),
    s: new Set([1, 2, 3, 4, 5])
  }
}

run(a, b);
Enter fullscreen mode Exit fullscreen mode
Micro Diff:
[]

Just Diff:
[]

deep-object-diff:
{
  added: {},
  deleted: {},
  updated: {},
}

diff:
[
  {
    t: 2,
    p: [ "obj", "m" ],
    v: Map(2) {
      "x": 1,
      "y": 0,
    },
  }, {
    t: 2,
    p: [ "obj", "s" ],
    v: Set(5) {
      1,
      2,
      3,
      4,
      5,
    },
  }
]
Enter fullscreen mode Exit fullscreen mode

Performance

For performance evaluation, we have created a benchmark file with some objects in our repo.

The following table is the output.

β”Œβ”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   β”‚ Task Name        β”‚ ops/sec β”‚ Average Time (ns) β”‚ Margin β”‚ Samples β”‚
β”œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
+ 0 β”‚ diff             β”‚ 252,694 β”‚ 3957.346814404028 β”‚ Β±1.60% β”‚ 25270   β”‚
β”‚ 1 β”‚ microdiff        β”‚ 218,441 β”‚ 4577.892286564301 β”‚ Β±0.92% β”‚ 21845   β”‚
β”‚ 2 β”‚ deep-object-diff β”‚ 121,385 β”‚ 8238.188318642591 β”‚ Β±1.66% β”‚ 12139   β”‚
β”‚ 3 β”‚ just-diff        β”‚ 105,292 β”‚ 9497.35384615396  β”‚ Β±1.66% β”‚ 10530   β”‚
β”‚ 4 β”‚ deep-diff        β”‚ 160,802 β”‚ 6218.820533549017 β”‚ Β±1.59% β”‚ 16081   β”‚
β””β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope I have justified the Fast & Accurate aspect of the library.

We have FAQs section; you can learn more there.

Please try to use it in your next project and feel free to send your feedback & issues you encounter via GitHub issues.

Please don't forget to check out our important Articles:

Happy coding! πŸš€

πŸ™ Thanks for reading.

πŸ’– πŸ’ͺ πŸ™… 🚩
thangaganapathy
Thanga Ganapathy

Posted on May 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related