Skip to content

Commit

Permalink
Version 5 (#95)
Browse files Browse the repository at this point in the history
* move to `module` infrastructure, and modern build setup

* get non-custom implementation working

* fix benchmarks and make `cache` generic

* get custom working as expected

* finish tests and typing

* add `arePrimitiveWrappersEqual` handler

* bind `toString` for better performance

* simplify custom class comparison

* move to `tinybench` for benchmarking

* inline `isPlainObject` and `isPromiseLike`

* remove duplicate `mixed types` benchmark

* add strict exports, and centralize custom equality

* add tests for `strictDeepEqual`, specifically testing the issue use-cases

* update recipes for custom equality

* update dependencies to latest

* include strict in tests, and add to benchmarks

* reduce `Map` / `Set` logic size with simpler gate

* update dependencies to latest

* avoid variable assignment for react check

* compare descriptors with strict mode, and update README

* update CHANGELOG with description of v5

* Release 5.0.0-beta.0

* add test based on existing issue

* include checking of `Symbol` keys in the documentation for strict checks

* include `index.d.ts` fallback location

* Release 5.0.0-beta.1

* eliminate imports to runtime files in `index.d.ts`, per feedback

* Release 5.0.0-beta.2

* use `createCustomEqual` for all equality comparator constructions, and support better merging and circular handling

* cleaner custom state usage

* restore simple config building, and avoid storage of entire `options` in `createCustomEqual`

* Release 5.0.0-beta.3

* include `engines` in `package.json`

* add TypedArray support

* remove `BigInt64Array` / `BigUint64Array` from type to make it easier for older consumers (but still support it at runtime)

* add documentation for `areTypedArraysEqual` and call it out in CHANGELOG

* leverage `isView` instead of tag check

* if in strict mode, `constructor` must match

* put strict check after POJO check to fast-path a common use-case

* Release 5.0.0-beta.4

* simplify the `areTypedArraysEqual` check

* update dependencies

* simplify the description of support in README

* Release 5.0.0-beta.5

* ensure constructors are equal, and fast-path common constructor types

* loose comparison to `null` seems to be faster than `!`

* might as well do loose null comparson for `isTypedArray`

* clean up typing

* renames, better typing, and update README to reflect `createCustomEqual` options change

* clean up recipes with changes

* add tests to validate TypedArray class differences

* update documentation for `constructor` equality checks

* more CHANGELOG tweaks

* improve README documentation, and set `fast-deep-equal` comparison to be more apples-to-apples (es6 + react)

* use iterator-based methods for `Map` / `Set` comparison

* update dependencies

* Release 5.0.0-beta.6

* add more code comments for better IDE feedback

* update dependencies to latest

* update benchmarks in README
  • Loading branch information
planttheidea committed Mar 5, 2023
1 parent e6b76c8 commit acd7bcb
Show file tree
Hide file tree
Showing 56 changed files with 4,493 additions and 3,549 deletions.
34 changes: 34 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"env": {
"lib": {
"presets": [
[
"@babel/preset-env",
{
"loose": true
}
]
]
},
"test": {
"presets": [
[
"@babel/preset-env",
{
"loose": true
}
]
]
}
},
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{
"loose": true,
"modules": false
}
]
]
}
40 changes: 19 additions & 21 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
{
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": ["plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"max-len": 0,
"operator-linebreak": 0,
"no-plusplus": 0,
"no-proto": 0,
"no-self-compare": 0,
"no-underscore-dangle": 0,
"no-unused-vars": 0,
"valid-typeof": 0,
"prefer-rest-params": 0,

"import/extensions": 0,
"import/prefer-default-export": 0,

"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-explicit-any": 0
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-non-null-assertion": 0
},
"settings": {
"react": {
"version": "detect"
}
}
}
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
# fast-equals CHANGELOG

## 5.0.0

### Breaking changes

#### `constructor` equality now required

To align with other implementations common in the community, but also to be more functionally correct, the two objects being compared now must have equal `constructor`s.

#### `Map` / `Set` comparisons no longer support IE11

In previous verisons, `.forEach()` was used to ensure that support for `Symbol` was not required, as IE11 did not have `Symbol` and therefore both `Map` and `Set` did not have iterator-based methods such as `.values()` or `.entries()`. Since IE11 is no longer a supported browser, and support for those methods is present in all browsers and Node for quite a while, the comparison has moved to use these methods. This results in a ~20% performance increase.

#### `createCustomEqual` contract has changed

To better facilitate strict comparisons, but also to allow for `meta` use separate from caching, the contract for `createCustomEqual` has changed. See the [README documentation](./README.md#createcustomequal) for more details, but froma high-level:

- `meta` is no longer passed through to equality comparators, but rather a general `state` object which contains `meta`
- `cache` now also lives on the `state` object, which allows for use of the `meta` property separate from but in parallel with the circular cache
- `equals` is now on `state`, which prevents the need to pass through the separate `isEqual` method for the equality comparator

#### `createCustomCircularEqual` has been removed

You can create a custom circular equality comparator through `createCustomEqual` now by providing `circular: true` to the options.

#### Custom `meta` values are no longer passed at callsite

To use `meta` properties for comparisons, they must be returned in a `createState` method.

#### Deep links have changed

If you were deep-linking into a specific asset type (ESM / CJS / UMD), they have changed location.

**NOTE**: You may no longer need to deep-link, as [the build resolution has improved](#better-build-system-resolution).

### Enhancements

#### New "strict" comparators available

The following new comparators are available:

- `strictDeepEqual`
- `strictShallowEqual`
- `strictCircularDeepEqual`
- `strictCircularShallowEqual`

This will perform the same comparisons as their non-strict counterparts, but will verify additional properties (non-enumerable properties on objects, keyed objects on `Array` / `Map` / `Set`) and that the descriptors for the properties align.

#### `TypedArray` support

Support for comparing all typed array values is now supported, and you can provide a custom comparator via the new `areTypedArraysEqual` option in the `createCustomEqual` configuration.

#### Better build system resolution

The library now leverages the `exports` property in the `package.json` to provide builds specific to your method of consumption (ESM / CommonJS / UMD). There is still a minified UMD version available if you want to use it instead.

#### `arePrimitiveWrappersEqual` option added to `createCustomEqual` configuration

If you want a custom comparator for primitive wrappers (`new Boolean()` / `new Number()` / `new String()`) it is now available.

## 4.0.3

- Remove unnecessary second strict equality check for objects in edge-case scenarios
Expand Down
138 changes: 70 additions & 68 deletions DEV_ONLY/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
/* globals document */

import * as React from 'react';

import {
createCustomEqual,
createCustomCircularEqual,
circularDeepEqual,
circularShallowEqual,
deepEqual,
shallowEqual,
strictDeepEqual,
strictShallowEqual,
} from '../src';

import type { BaseCircularMeta } from '../index.d';

document.body.style.backgroundColor = '#1d1d1d';
document.body.style.color = '#d5d5d5';
document.body.style.margin = '0px';
Expand Down Expand Up @@ -232,19 +229,16 @@ const object4 = {
zero: 0,
};

const doesNotEverEqualOne = createCustomEqual<undefined>((defaultOptions) => {
return {
...defaultOptions,
createIsNestedEqual(comparator) {
return (a: any, b: any) => {
if (typeof a === 'number' || typeof b === 'number') {
return a !== 1 && b !== 1;
}

return Object.keys(a).every((key) => comparator(a[key], b[key]));
};
const doesNotEverEqualOne = createCustomEqual<undefined>({
createInternalComparator:
(comparator) =>
(a: any, b: any, _keyA, _keyB, _parentA, _parentB, state) => {
if (typeof a === 'number' || typeof b === 'number') {
return a !== 1 && b !== 1;
}

return Object.keys(a).every((key) => comparator(a[key], b[key], state));
},
};
});

console.log('true', doesNotEverEqualOne(object1, object2));
Expand Down Expand Up @@ -291,47 +285,13 @@ console.groupEnd();

console.group('custom circular');

interface CustomCircularCache extends BaseCircularMeta {
interface CustomCircularMeta {
customMethod(value: any): void;
customValue: string;
}

function getCustomCircularCache(): CustomCircularCache {
const entries: [object, object][] = [];

function getCustomCircularCache(): CustomCircularMeta {
return {
delete(key: object) {
for (let index = 0; index < entries.length; ++index) {
if (entries[index][0] === key) {
entries.splice(index, 1);
return true;
}
}

return false;
},

get(key: object) {
for (let index = 0; index < entries.length; ++index) {
if (entries[index][0] === key) {
return entries[index][1];
}
}
},

set(key: object, value: object) {
for (let index = 0; index < entries.length; ++index) {
if (entries[index][0] === key) {
entries[index][1] = value;
return this;
}
}

entries.push([key, value]);

return this;
},

customMethod(value) {
console.log('hello', value);
},
Expand All @@ -351,17 +311,20 @@ function areRegExpsEqual(a: RegExp, b: RegExp) {
);
}

const customDeepEqualCircularHandler =
createCustomCircularEqual<CustomCircularCache>((defaultOptions) => ({
areObjectsEqual(a, b, isEqual, cache) {
cache.customMethod(cache.customValue);
const customDeepEqualCircular = createCustomEqual<CustomCircularMeta>({
circular: true,
createCustomConfig: (defaultOptions) => ({
areObjectsEqual(a, b, state) {
state.meta.customMethod(state.meta.customValue);

return defaultOptions.areObjectsEqual(a, b, isEqual, cache);
return defaultOptions.areObjectsEqual(a, b, state);
},
areRegExpsEqual,
}));
const customDeepEqualCircular = (a: any, b: any) =>
customDeepEqualCircularHandler(a, b, getCustomCircularCache());
}),
createState: () => ({
meta: getCustomCircularCache(),
}),
});

console.log(
'true',
Expand All @@ -380,22 +343,23 @@ console.groupEnd();

console.group('targeted custom');

const isDeepEqualOrFooMatchesMeta = createCustomEqual<'bar'>(() => ({
createIsNestedEqual:
(deepEqual) => (a, b, _keyA, _keyB, _parentA, _parentB, meta) =>
a === meta || b === meta || deepEqual(a, b, meta),
}));
const isDeepEqualOrFooMatchesMeta = createCustomEqual<'bar'>({
createInternalComparator:
(compare) => (a, b, _keyA, _keyB, _parentA, _parentB, state) => {
return a === state.meta || b === state.meta || compare(a, b, state);
},
createState: () => ({ meta: 'bar' }),
});

console.log(
'shallow',
isDeepEqualOrFooMatchesMeta({ foo: 'bar' }, { foo: 'baz' }, 'bar'),
isDeepEqualOrFooMatchesMeta({ foo: 'bar' }, { foo: 'baz' }),
);
console.log(
'deep',
isDeepEqualOrFooMatchesMeta(
{ nested: { foo: 'bar' } },
{ nested: { foo: 'baz' } },
'bar',
),
);

Expand Down Expand Up @@ -485,3 +449,41 @@ console.log(
);

console.groupEnd();

console.group('typed arrays');

console.log(
'true - deep',
deepEqual(new Int8Array([21, 31]), new Int8Array([21, 31])),
);
console.log(
'true - shallow',
shallowEqual(new Int8Array([21, 31]), new Int8Array([21, 31])),
);

console.log(
'true - strict deep',
strictDeepEqual(new Int8Array([21, 31]), new Int8Array([21, 31])),
);
console.log(
'true - strict shallow',
strictShallowEqual(new Int8Array([21, 31]), new Int8Array([21, 31])),
);

console.log(
'true - deep (different types)',
deepEqual(new Int8Array([21, 31]), new Int16Array([21, 31])),
);
console.log(
'true - shallow (different types)',
shallowEqual(new Int8Array([21, 31]), new Int16Array([21, 31])),
);

console.log(
'false - strict deep (different types)',
strictDeepEqual(new Int8Array([21, 31]), new Int16Array([21, 31])),
);
console.log(
'false - strict shallow (different types)',
strictShallowEqual(new Int8Array([21, 31]), new Int16Array([21, 31])),
);
Loading

0 comments on commit acd7bcb

Please sign in to comment.