Giter Club home page Giter Club logo

microdiff's Introduction

Microdiff Logo

Microdiff is a tiny (currently <1kb), fast, zero dependency object and array comparison library. It is significantly faster than most other deep comparison libraries, and has full TypeScript support.

💡 I recommend reading this blog post:

Building the fastest object and array differ for an explanation of how Microdiff achieves its size and speed.

Minizipped Size (from Bundlephobia) License dependency Count

Features

  • 🚀 More than double the speed of other object diff libraries
  • 📦 Extremely lightweight, <1kb minified
  • 🌎 Supports Deno, Node, the web, and even service workers. Also comes with built-in Typescript types
  • 🔰 Very easy to use, having just a single diff() function
  • 📅 Full support for objects like new Date() and new RegExp()

Get started

First, install Microdiff

npm i microdiff

If you are using Deno, you can import it from Deno.land with the link https://deno.land/x/microdiff@VERSION/index.ts (remember to change @VERSION to the version you want to use).

After you install it, import it and run it on two objects.

import diff from "microdiff";

const obj1 = {
	originalProperty: true,
};
const obj2 = {
	originalProperty: true,
	newProperty: "new",
};

console.log(diff(obj1, obj2));
// [{type: "CREATE", path: ["newProperty"], value: "new"}]

If you are using CommonJS, you can import it like this:

const diff = require("microdiff").default;

There are three different types of changes: CREATE, REMOVE, and CHANGE. The path property gives a path to the property in the new object (or the old object in the case of REMOVE). Each element in the paths is a key to the next property a level deeper until you get to the property changed, and it is a string or a number, depending on whether the object is an Array or Object (Objects with number keys will still be strings). The value property exists in types CREATE and CHANGE, and it contains the value of the property added/changed/deleted. The oldValue property exists in the type CHANGE and REMOVE, and it contains the old value of the property.

Cycles support

By default, Microdiff supports cyclical references, but if you are sure that the object has no cycles like parsed JSON, you can disable cycles using the cyclesFix option.

diff(obj1, obj2, { cyclesFix: false });

Benchmarks

Benchmarks: Small object
deep-diff: 17929ns - 409% slower
deep-object-diff: 10763ns - 206% slower
jsdiff: 79700ns - 2164% slower
microdiff: 3520ns - Fastest

Benchmarks: Large Object
deep-diff: 272887ns - 259% slower
deep-object-diff: 160019ns - 111% slower
jsdiff: 1688294ns - 2123% slower
microdiff: 75934ns - Fastest

These benchmarks are currently only for one small object and a very large object, so they might not be accurate. I will be working on creating benchmarks with more varying types.

Contributing

Thanks for helping the project out! Contributing is pretty simple. Fork the repository (if you need more information on how to do this, check out this GitHub guide), clone it to your computer, and start programming! To compile the program, run npm run build (replace npm with pnpm or yarn if you are using one of those). This will create CommonJS and ESM modules in /dist.

To benchmark microdiff, you can run npm run bench. This will automatically build Microdiff and run a benchmarking program comparing microdiff to other common diffing libraries.

Finally, Microdiff has an extensive test suite which you should take advantage of. To make sure everything is working correctly, you can run npm run test. npm run test builds the project and then runs the entire test suite on the new version. If you are fixing a bug, be sure to add a test for that. Also, make sure you read the Code of Conduct before contributing.

microdiff's People

Contributors

annatarhe avatar asyncbanana avatar brainthinks avatar danialdezfouli avatar denis-bel avatar garrett-wombat avatar micalevisk avatar schrubitteflau avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

microdiff's Issues

Add templates for issues

Issue templates are very helpful for a collaboration repo. When users identify a bug or want to add a new feature, you can provide templates so you can collect all the pertinent information you need to fix a bug or add a new feature.

We recommend creating a “Report Bug” and “Feature Request” issue template.

Some suggested prompts/questions you can add to a “Report Bug” template are:

  • Briefly describe the bug
  • What is the expected behavior?
  • Please provide step by step instructions on how to reproduce the bug

Some suggested prompts/questions you can add to a “Feature Request” issue template are:

  • Briefly describe your feature request
  • What problem is this feature trying to solve?
  • How do we know when the feature is complete?

Circular references

I don't think this should be supported, because of the extra overhead either in performance or in API complexity, but I think the documentation should state clearly that it's not handled. I mean, reading the code this is obvious, but it would still be a good service :)

P.s. do you align with the handmade manifesto? If you haven't heard of it, do check it out, because it seems you do! 😊🙌

Cheers

Add Code of Conduct

We recommend that every repo has a code of conduct. If you don’t feel comfortable creating your own Code of Conduct from scratch we highly recommend using one of the templates provided by GitHub. If you do use a template, please read through the template and ensure that you can and will abide by the Code of Conduct.

Please follow these instructions on how to add a Code of Conduct.

Loops / infinite recursion

If you try to diff structures that have a loop in it, your diff algorithm will hang and enter infinite recursion and therefore exhaust system resources until the tab or the process hangs.

Does not handle object changes within array

import diff from "microdiff";

const obj1 = {
	array:[{test2:0}]
};
const obj2 = {
	array:[{test2:1}]
};

console.log(diff(obj1, obj2));

This ^ console logs an empty array. (repl)

Are there any plans on supporting objects within arrays? If no, do you know about another library that do support this?

Community Exchange Introduction/Tracking

👋 Hi @AsyncBanana,

I am your GitHub mentor for the GitHub Education, Community Exchange (CX) project. I'll be collaborating with you on preparing your repo for CX.

You mentioned in your submission that you wanted to submit a Collaborate repo. This means that you want to invite other students to collaborate and add features to this repo. I will generate issues, which will provide guidance on how to prepare your repo for a Collaborate CX submission on June 1, 2022.

This issue will serve as a tracking issue to track all issues related to CX. I recommend creating a new branch for every issue and opening a pull request to track changes so we can effectively collaborate with each other and merge changes when you and I feel like those changes are ready to be merged on your primary branch.

If you have any questions or concerns, please feel free to leave a comment on this issue or any of the other issues that are generated.

I look forward to working with you :octocat:

Issues

UMD bundle

Good day, and thanks for such awesome library! Are you planning to add separate builds like many other libraries do? I use your library for development under an old browser that doesn't support CommonJS format, and I constantly have to do custom UMD builds using rollup or other tools. It will be nice if you can add separate builds and possibly links to the CDN. I can try PR and implement it myself, but I'm very busy right now.

Recreation from diff

Hey!

I'm currently considering to use microdiff for my project, however I also have to be able to recreate an object from the diff and the source object (basically so that im able to send smaller-sized updates to a client state via a websocket connection), but it seems like microdiff is not offering any method for this. I will implement this in my project but I would like to know if this is in the ToDo.

CommonJS support

I'm trying to require('microdiff') but it throws ERR_REQUIRE_ESM. I'm using Node.js 12. Don't you think CommonJS should still be supported?

➕ Feature - Export DIFFERENCE Type so it can be imported in Ts projects

❗ Is your feature request related to a problem? Please describe.
Yes. I cannot use the ReturnType in Typescript because it fails with this error:

Exported variable 'myFn' has or is using name 'DifferenceCreate' from external module "node_modules/microdiff/dist/index" but cannot be named.ts(4023)

✅ Describe the solution you'd like
I believe simply exporting all the types would be sufficient, or putting them under the "microdiff" module.

↗️ Describe alternatives you've considered
None yet

➕ Additional context
This happens with Typescript 4.9

Cheers!

Inconsistent diff Behavior for Object-derived Type Changes

Issue Summary:

The diff function exhibits inconsistent behavior when comparing changes between properties of different Object-derived types such as Array, Set, Map, etc. It appears to misidentify certain type changes as property additions/removals rather than recognizing a complete change in the property's type.

Example:

const changes = diff({ data: [] }, { data: { val: 'test' } });
// Returned: [ { type: 'CREATE', path: ['data', 'val'], value: 'test' } ]
// Expected: [ { type: 'CHANGE', path: ['data'], value: { val: 'test' } } ]

Description of the bug:

When the diff function encounters property values that have changed type—specifically from an Array, Set, Map, Error, Promise, or Proxy to another object type—it incorrectly reports the changes as individual property mutations (CREATE and REMOVE) rather than recognizing a CHANGE in the entire value for the key.

For types such as Function, Date, and RegExp, when these are nested property values, the change is correctly detected as a CHANGE. However, when types like Array, Set, or Map are nested, they do not receive the same treatment.

Additional Context:

There's a discrepancy based on whether the Object-derived types are passed directly as an argument to diff or nested. For example:

const diffArgument = diff(new Date(), { data: 'test' });
// Returned: [ { type: 'CREATE', path: ['data'], value: 'test' } ]

const diffObjValue = diff({ data: new Date() }, { data: { val: 'test' } });
// Returned: [ { type: 'CHANGE', path: ['data'], value: { val: 'test' }, oldValue: '2023-11-06T02:40:05.198Z' }]

It is not clear if this behavior is intentional or an oversight. Clarification in the documentation would be beneficial.

get array removals as "DELETE"

Hey, first of all, great little tool!
But I have a question: I have two arrays, and I want to see if something gets updated, or removed or added to the array.
In both arrays are JS Objects, and if on of the JS objects is removed, I want that the libary tells met that one thing got removed, out of one array, but instead what is happening is that I get a "change" for every entry because the index of the other objects has changed. Is there a way to realise this?

Thanks

this is extractly what i need, i had a protocol impl before, but not completed like yours. i decide to use your version to build a small website

Update README.md

Your README is in great shape! 🎉

I would recommend adding:

Depending on the complexity of your project, you might be interested in creating a CONTRIBUTING.md file, but we recommend starting simple with the bullet points above, and once you start to see an increase in users interested in contributing, you should consider a CONTRIBUTING.md file so you don’t find yourself overwhelmed with questions from other users.

Possible optimization: hashing

Leverage hashing for detecting change much much faster than a deep comparison?
And microdiff would have optionally a backing buffer/cache of previous object hashes?

support for NaN values

Currently, NaN values always result in CHANGE, even though nothing changed. Technically in JS, NaN !== NaN, so I understand that there is an argument for this behavior. However, I think that having a CHANGE entry on EVERY diff when an object contains NaN is noise, and therefore unexpected / undesirable behavior.

I propose treating NaN values as equivalent, resulting in the following:

diff({ testNaN: NaN }, { testNaN: NaN }) === [] // true

Is it possible to see the old value?

Hello, it would be very nice to be able to see the old value in a CHANGE operation. Is it already possible? If not I think it would be easy to add right?

Cannot use 'in' operator to search for ... in null

const diff = require('microdiff').default
const a1 = {"a": { "b": 1 }}
const a2 = {"a": null}
console.log(diff(a1, a2))
/Users/.../node_modules/microdiff/dist/index.cjs:8
		if (!(key in newObj)) {
		          ^

TypeError: Cannot use 'in' operator to search for 'b' in null

Extend the diff function with rich types not included in the base JS language.

❗ Is your feature request related to a problem? Please describe.
We use started using microdiff as a pre-step before doing db updates. When working with MongoDB, then use the ObjectId data type which is exported by the bson package to keep track of documents (records) and references between data.
It can be compared in JS using the toString() (or casting it to string).

However, currently microdiff compares it as an object and because the object has no properties (only methods), it can not identify the change.

✅ Describe the solution you'd like
In order for us to solve this issue and have microdiff detect changes to properties that are ObjectId, we needed to duplicate the function to our code base and extend the rich types object with the ObjectId: true property to have it trigger the "rich type" check and be evaluated as two strings being compared with each other.

↗️ Describe alternatives you've considered
I would recommend adding a property to the third argument of the function where cyclesFix can be passed in. This would be an object of extended rich types which then also need to be checked while calling the diff function.

➕ Additional context
Other data types in JS which have a compatible (to String) method of comparing them can be added to this and thus we can still use the microdiff library without having to duplicate the functionality.

How about having index path for array be a number instead of a string?

<script>
import diff from "microdiff";

const array1 = [1];
const array2 = [2];
	
	
const obj1 = {0:1};
const obj2 = {0:2};

</script>

{JSON.stringify(diff(array1,array2))}
<br/>
{JSON.stringify(diff(obj1,obj2))}

Both diffs give the same result, which could be problematic:

[{"path":["0"],"type":"CHANGE","value":2}]
[{"path":["0"],"type":"CHANGE","value":2}]

(repl)

My proposal is to differenciate between object and array by having array indexes be a number instead of a string, so that the result would be this instead:

[{"path":[0],"type":"CHANGE","value":2}]
[{"path":["0"],"type":"CHANGE","value":2}]

So the path array will have this type:

 Array<string | number>

This would make it easier to create a patching function too

Ineficient change detection

I need to detect as few changes as possible, however instead of one change I get a couple.

const changes = diff(['0', { a: 1 }], ['0', '1', { a: 1 }])

const expected = [ { type: 'CREATE', path: [ 1 ], value: '1' }]

const actual = [
  { path: [ 1 ], type: 'CHANGE', value: '1', oldValue: { a: 1 } },
  { type: 'CREATE', path: [ 2 ], value: { a: 1 } }
]

Is it expected behaviour? I have to use a double for loop to merge these changes back - that is not good for performance

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.