π The Fast, Accurate, JavaScript Objects Diffing & Patching Library
Thanga Ganapathy
Posted on May 1, 2024
Welcome,
I am here to introduce the new JavaScript library for objects diffing & patching.
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
- Plain Objects, e.g.
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.
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);
}
}
Let's start our complete testing.
1. Simple with no difference between objects
run({}, {});
Micro Diff:
[]
Just Diff:
[]
deep-object-diff:
{
added: {},
deleted: {},
updated: {},
}
diff:
[]
2. Simple with single difference
run({ a: 1 }, { a: 2 });
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,
}
]
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);
Micro Diff:
[]
Just Diff:
[]
deep-object-diff:
{
added: {},
deleted: {},
updated: {},
}
diff:
[]
All Working fine, no issues.
4. Number properties
const a = [NaN, Infinity, -Infinity];
const b = [NaN, Infinity, Infinity];
run(a, b);
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,
}
]
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);
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",
}
]
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);
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 ],
}
]
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);
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,
}
]
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);
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,
},
}
]
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 β
βββββ΄βββββββββββββββββββ΄ββββββββββ΄ββββββββββββββββββββ΄βββββββββ΄ββββββββββ
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.
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
November 30, 2024
November 30, 2024