├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── .release-it.beta.json ├── .release-it.json ├── CHANGELOG.md ├── DEV_ONLY └── index.tsx ├── LICENSE ├── README.md ├── __tests__ ├── __helpers__ │ ├── values.ts │ └── words.json ├── hash.ts ├── index.ts └── stringify.ts ├── benchmarks ├── compare.js ├── index.js └── tests.js ├── build ├── rollup │ ├── config.base.js │ ├── config.cjs.js │ ├── config.esm.js │ ├── config.min.js │ └── config.umd.js ├── tsconfig │ ├── base.json │ ├── cjs.json │ ├── declarations.json │ ├── esm.json │ ├── min.json │ └── umd.json └── webpack.config.js ├── index.d.ts ├── jest.config.js ├── package.json ├── results_2.1.2.txt ├── results_3.0.0.txt ├── results_4.1.0.txt ├── results_5.0.2.txt ├── results_latest.txt ├── src ├── cache.ts ├── constants.ts ├── hash.ts ├── index.ts ├── sort.ts ├── stringify.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "lib": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "loose": true 9 | } 10 | ] 11 | ] 12 | }, 13 | "test": { 14 | "presets": [ 15 | [ 16 | "@babel/preset-env", 17 | { 18 | "loose": true 19 | } 20 | ] 21 | ] 22 | } 23 | }, 24 | "presets": [ 25 | "@babel/preset-typescript", 26 | [ 27 | "@babel/preset-env", 28 | { 29 | "loose": true, 30 | "modules": false 31 | } 32 | ] 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2021, 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["@typescript-eslint"], 14 | "rules": { 15 | "prefer-rest-params": 0, 16 | 17 | "@typescript-eslint/explicit-function-return-type": 0, 18 | "@typescript-eslint/explicit-module-boundary-types": 0, 19 | "@typescript-eslint/no-explicit-any": 0, 20 | "@typescript-eslint/no-non-null-assertion": 0 21 | }, 22 | "settings": { 23 | "react": { 24 | "version": "detect" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .nyc_output 3 | coverage 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | .gitignore 4 | .npmignore 5 | .prettierrc 6 | .release-it*.json 7 | __tests__ 8 | benchmarks 9 | build 10 | coverage 11 | DEV_ONLY 12 | node_modules 13 | src 14 | jest.config.js 15 | *.csv 16 | results_*.txt 17 | tsconfig.json 18 | yarn* 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.release-it.beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "npm": { 7 | "tag": "next" 8 | }, 9 | "preReleaseId": "beta", 10 | "hooks": { 11 | "before:init": [ 12 | "npm run lint", 13 | "npm run typecheck", 14 | "npm run test", 15 | "npm run build" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": [ 8 | "npm run lint", 9 | "npm run typecheck", 10 | "npm run test", 11 | "npm run build" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # hash-it CHANGELOG 2 | 3 | ## 6.0.0 4 | 5 | **Breaking changes** 6 | 7 | - Equality utilities (`is` / `is.any` / `is.all` / `is.not`) are no longer provided 8 | - `Error` type hashes now include the message (previously only included stack) 9 | - Non-enumerable type hashes (`Generator`, `Promise`, `WeakMap`, `WeakSet`) now hash uniquely based on reference 10 | - `WeakMap` is now required at runtime (used as cache for circular references) 11 | 12 | **Enhancements** 13 | 14 | - Better support for system-specific loading (ESM vs CJS vs UMD) 15 | - Added support for primitive wrappers (e.g. `new Number('123')`) 16 | - Added support for more object classes 17 | - `AsyncFunction` 18 | - `AsyncGeneratorFunction` 19 | - `BigInt64Array` 20 | - `BigUint64Array` 21 | - `GeneratorFunction` 22 | - `SharedArrayBuffer` 23 | - `WeakRef` (same limitations as those for `WeakMap` / `WeakSet`) 24 | 25 | ## 5.0.2 26 | 27 | - Reduce code size by 29.29% (19.42% gzipped size) 28 | - Activate strict mode for typing 29 | 30 | ## 5.0.1 31 | 32 | - Update `.npmignore` to reduce package tarball size ([#39](https://github.com/planttheidea/hash-it/pull/39)) 33 | 34 | ## 5.0.0 35 | 36 | ### Breaking changes 37 | 38 | - Remove autocurrying of `hash.is` methods 39 | - Remove transpiled builds in favor of rollup distributed files (deep-linking will no longer work) 40 | 41 | ### Enhancements 42 | 43 | - Codebase rewritten in TypeScript 44 | - Added `BigInt` support 45 | 46 | ## 4.1.0 47 | 48 | - Add TypeScript definitions 49 | - Significant speed improvements 50 | 51 | ## 4.0.5 52 | 53 | - Fix issues related to string encoding and collisions 54 | [#23](https://github.com/planttheidea/hash-it/issues/23) 55 | 56 | ## 4.0.4 57 | 58 | - Improve speed of complex objects (Objects, Arrays, Maps, Sets) 59 | - Fix security issue with old version of `webpack-dev-server` 60 | 61 | ## 4.0.3 62 | 63 | - Upgrade to use babel 7 for builds 64 | 65 | ## 4.0.2 66 | 67 | - Fix [#18](https://github.com/planttheidea/hash-it/pull/18) - IE11 not allowing global `toString` 68 | to be used, instead using `Object.prototype.toString` (thanks 69 | [@JorgenEvens](https://github.com/JorgenEvens)) 70 | 71 | ## 4.0.1 72 | 73 | - Remove unused values from publish 74 | 75 | ## 4.0.0 76 | 77 | Rewrite! Lots of changes under-the-hood for a much more consistent hash, and circular object 78 | handling out of the box. 79 | 80 | #### BREAKING CHANGES 81 | 82 | - `isEmpty`, `isEqual`,`isNull`, and `isUndefined` have been removed (all can be reproduced with new 83 | `is` and `is.all` functions) 84 | - `hash.isNull` => `hash.is(null)` 85 | - `hash.isUndefined` => `hash.is(undefined)` 86 | - `hash.isEqual` => `hash.is.all` 87 | - `hash.isEmpty` => 88 | `(object) => hash.is.any(object, undefined, null, '', 0, [], {}, new Map(), new Set())` 89 | - `Error` hashes now based on `error.stack` instead of `error.message` 90 | 91 | #### ENHANCEMENTS 92 | 93 | - Circular objects are now handled out of the box, thanks to 94 | [`fast-stringify`](https://github.com/planttheidea/fast-stringify) 95 | - Collision rates are near-zero (previously used traditional DJB2, which has small collision rates) 96 | - Better `ArrayBuffer` support with the use of `Buffer.from` when supported 97 | - SVG elements, DocumentFragments, and Events are now supported 98 | - `is` partial-application function allows for easy creation of any type of `isEqual` comparison 99 | method 100 | - `is.any` performs the same multiple-object check that `is.all` does, but only checks if one of the 101 | other objects is equal 102 | - `is.not` performs the same comparison that `is` does, but checks for non-equality 103 | 104 | #### FIXES 105 | 106 | - `Object` / `Map` / `Set` no longer returns different hashes based on order of key addition 107 | - `hash.isEqual` will no longer fail if nothing is passed 108 | 109 | ## 3.1.2 110 | 111 | - Remove extraneous `toString` call (performance) 112 | 113 | ## 3.1.1 114 | 115 | - Improve hash uniqueness for HTML elements 116 | 117 | ## 3.1.0 118 | 119 | - Add support for `Generator` (not just `GeneratorFunction`) 120 | - Streamline `typeof`- vs `toString`-driven handling for improved speed for most types 121 | 122 | ## 3.0.0 123 | 124 | - Improve speed (2-4x faster depending on type) 125 | - Smaller footprint (~25% reduction) 126 | - Improve hash accuracy for functions (hash now includes function body) 127 | - Fix issue where stack remained in memory after hash was built 128 | - Add ES transpilation for module-ready build tools 129 | 130 | #### BREAKING CHANGES 131 | 132 | - If using CommonJS, you need to specify `require('hash-it').default` instead of just 133 | `require('hash-it')` 134 | - Hashes themselves may have changed (especially for circular objects) 135 | - Removed `isRecursive` method on `hashIt` object (which was wrongly named to begin with) 136 | - To specifically handle _circular_ objects (which is what it really did), pass `true` as the 137 | second parameter to `hashIt` 138 | 139 | ## 2.1.2 140 | 141 | - Move up isNull check in replacer (improve performance of more likely use-case) 142 | 143 | ## 2.1.1 144 | 145 | - Create isNull utility instead of checking strict equality in multiple places 146 | 147 | ## 2.1.0 148 | 149 | - Overall speed improvement by an average of 18.74% (35.27% improvement on complex objects) 150 | 151 | ## 2.0.1 152 | 153 | - More speed improvements 154 | 155 | ## 2.0.0 156 | 157 | - Use JSON.stringify with replacer as default, without try/catch 158 | - Move use of try/catch with fallback to prune to new `hashIt.withRecursion` method (only necessary 159 | for deeply-recursive objects like `window`) 160 | - Reorder switch statement for commonality of use cases 161 | - Leverage typeof in switch statements when possible for performance 162 | 163 | ## 1.3.1 164 | 165 | - Add optimize-js plugin for performance in script version 166 | 167 | ## 1.3.0 168 | 169 | - Add hashIt.isUndefined, hashIt.isNull, and hashIt.isEmpty methods 170 | - Reorder switch statements in replacer and getValueForStringification to reflect most likely to 171 | least likely (improves performance a touch) 172 | - Remove "Number" from number stringification 173 | - Leverage prependTypeToString whereever possible 174 | - Include Arguments object class 175 | 176 | ## 1.2.1 177 | 178 | - Calculation of Math hashCode now uses properties 179 | - Fix README 180 | 181 | ## 1.2.0 182 | 183 | - Add hashIt.isEqual method to test for equality 184 | - Prepend all values not string or number with object class name to help avoid collision with 185 | equivalent string values 186 | 187 | ## 1.1.0 188 | 189 | - Add support for a variety of more object types 190 | - Fix replacer not using same stringifier for int arrays 191 | 192 | ## 1.0.0 193 | 194 | - Initial release 195 | -------------------------------------------------------------------------------- /DEV_ONLY/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import hash from '../src'; 4 | 5 | document.body.style.backgroundColor = '#1d1d1d'; 6 | document.body.style.color = '#d5d5d5'; 7 | document.body.style.margin = '0px'; 8 | document.body.style.padding = '0px'; 9 | 10 | const fragment = document.createDocumentFragment(); 11 | 12 | fragment.appendChild(document.createElement('div')); 13 | fragment.appendChild(document.createElement('ul')); 14 | fragment.appendChild(document.createElement('span')); 15 | 16 | class StatefulComponent extends React.Component { 17 | render() { 18 | return
test
; 19 | } 20 | } 21 | 22 | const StatelessComponent = () =>
test
; 23 | 24 | const a: Record = { 25 | foo: 'bar', 26 | }; 27 | 28 | const b = { 29 | a, 30 | }; 31 | 32 | a.b = b; 33 | 34 | function getArguments() { 35 | return arguments; 36 | } 37 | 38 | const object = { 39 | ReactStatefulClass: StatefulComponent, 40 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 41 | // @ts-ignore 42 | ReactStatefulElement: , 43 | ReactStatelessClass: StatelessComponent, 44 | ReactStatelessElement: , 45 | // @ts-expect-error - arguments are not defined 46 | arguments: getArguments('foo', 'bar'), 47 | arr: ['foo', 'bar'], 48 | arrayBuffer: new Uint16Array([1, 2, 3]).buffer, 49 | bigint: BigInt(9007199254740991), 50 | bool: true, 51 | dataView: new DataView(new ArrayBuffer(2)), 52 | doc: document, 53 | el: document.createElement('div'), 54 | err: new Error('Stuff'), 55 | event: (() => { 56 | try { 57 | return new Event('custom'); 58 | } catch (error) { 59 | const event = document.createEvent('Event'); 60 | 61 | event.initEvent('custom', true, true); 62 | 63 | return event; 64 | } 65 | })(), 66 | float32Array: new Float32Array([1, 2, 3]), 67 | float64Array: new Float64Array([1, 2, 3]), 68 | fragment, 69 | func() { 70 | alert('y'); 71 | }, 72 | *generator(): Generator { 73 | const value = yield 1; 74 | 75 | yield value + 2; 76 | }, 77 | int8Array: new Int8Array([1, 2, 3]), 78 | int16Array: new Int16Array([1, 2, 3]), 79 | int32Array: new Int32Array([1, 2, 3]), 80 | map: new Map().set(true, 7).set({ foo: 3 }, ['abc']), 81 | math: Math, 82 | nil: null, 83 | nodeList: document.querySelectorAll('div'), 84 | num: 12, 85 | obj: { 86 | foo: 'bar', 87 | }, 88 | promise: Promise.resolve(1), 89 | regexp: /test/, 90 | set: new Set().add('foo').add(2), 91 | string: 'foo', 92 | svg: document.createElementNS('http://www.w3.org/2000/svg', 'svg'), 93 | symbol: Symbol('test'), 94 | uint8Array: new Uint8Array([1, 2, 3]), 95 | uint8ClampedArray: new Uint8ClampedArray([1, 2, 3]), 96 | uint16Array: new Uint16Array([1, 2, 3]), 97 | uint32Array: new Uint32Array([1, 2, 3]), 98 | undef: undefined, 99 | weakMap: new WeakMap().set({}, 7).set({ foo: 3 }, ['abc']), 100 | weakSet: new WeakSet().add({}).add({ foo: 'bar' }), 101 | win: window, 102 | }; 103 | 104 | // const profile = (iterations = 100) => { 105 | // let index = -1; 106 | 107 | // console.profile('hash timing'); 108 | 109 | // while (++index < iterations) { 110 | // hash(object); 111 | // hash(a); 112 | 113 | // for (const key in object) { 114 | // // @ts-expect-error - not worth it 115 | // hash(object[key]); 116 | // } 117 | 118 | // hash(document.body); 119 | // // this is the killer, so only profiled if you uncomment 120 | // // hash(window); 121 | // } 122 | 123 | // console.profileEnd('hash timing'); 124 | 125 | // console.log('Check the Profiles tab in DevTools to see the output.'); 126 | // }; 127 | 128 | // const benchmark = () => require('../benchmarks/index'); 129 | 130 | const visualValidation = () => { 131 | console.log(object, hash(object)); 132 | console.log(a, hash(a)); 133 | console.log(object.string, hash(object.string)); 134 | console.log(object.num, hash(object.num)); 135 | console.log(object.bigint, hash(object.bigint)); 136 | console.log(object.bool, hash(object.bool)); 137 | console.log(object.func, hash(object.func)); 138 | console.log(object.undef, hash(object.undef)); 139 | console.log(object.nil, hash(object.nil)); 140 | console.log(object.obj, hash(object.obj)); 141 | console.log(object.arr, hash(object.arr)); 142 | console.log(object.el, hash(object.el)); 143 | console.log(object.fragment, hash(object.fragment)); 144 | console.log(object.svg, hash(object.svg)); 145 | console.log(object.regexp, hash(object.regexp)); 146 | console.log(object.event, hash(object.event)); 147 | console.log(object.nodeList, hash(object.nodeList)); 148 | 149 | // comment out for older browser testing 150 | console.log(object.symbol, hash(object.symbol)); 151 | console.log(object.err, hash(object.err)); 152 | console.log(object.map, hash(object.map)); 153 | console.log(object.set, hash(object.set)); 154 | console.log(object.weakMap, hash(object.weakMap)); 155 | console.log(object.weakSet, hash(object.weakSet)); 156 | console.log(object.dataView, hash(object.dataView)); 157 | console.log(object.arrayBuffer, hash(object.arrayBuffer)); 158 | console.log(object.float32Array, hash(object.float32Array)); 159 | console.log(object.float64Array, hash(object.float64Array)); 160 | console.log(object.generator, hash(object.generator)); 161 | console.log(object.int8Array, hash(object.int8Array)); 162 | console.log(object.int16Array, hash(object.int16Array)); 163 | console.log(object.int32Array, hash(object.int32Array)); 164 | console.log(object.promise, hash(object.promise)); 165 | console.log(object.uint8Array, hash(object.uint8Array)); 166 | console.log(object.uint8ClampedArray, hash(object.uint8ClampedArray)); 167 | console.log(object.uint16Array, hash(object.uint16Array)); 168 | console.log(object.uint32Array, hash(object.uint32Array)); 169 | 170 | console.log(object.ReactStatefulClass, hash(object.ReactStatefulClass)); 171 | console.log(object.ReactStatefulElement, hash(object.ReactStatefulElement)); 172 | console.log(object.ReactStatelessClass, hash(object.ReactStatelessClass)); 173 | console.log(object.ReactStatelessElement, hash(object.ReactStatelessElement)); 174 | console.log(object.doc.body, hash(object.doc.body)); 175 | console.log(object.win, hash(object.win)); 176 | }; 177 | 178 | // const hashOnlyValidation = () => { 179 | // console.log(hash(object)); 180 | // console.log(hash(a)); 181 | // console.log(hash(object.string)); 182 | // console.log(hash(object.num)); 183 | // console.log(hash(object.bool)); 184 | // console.log(hash(object.func)); 185 | // console.log(hash(object.undef)); 186 | // console.log(hash(object.nil)); 187 | // console.log(hash(object.obj)); 188 | // console.log(hash(object.arr)); 189 | // console.log(hash(object.el)); 190 | // console.log(hash(object.fragment)); 191 | // console.log(hash(object.svg)); 192 | // console.log(hash(object.math)); 193 | // console.log(hash(object.regexp)); 194 | // console.log(hash(object.event)); 195 | // console.log(hash(object.nodeList)); 196 | 197 | // // comment out for older browser testing 198 | // console.log(hash(object.symbol)); 199 | // console.log(hash(object.err)); 200 | // console.log(hash(object.map)); 201 | // console.log(hash(object.set)); 202 | // console.log(hash(object.weakMap)); 203 | // console.log(hash(object.weakSet)); 204 | // console.log(hash(object.arrayBuffer)); 205 | // console.log(hash(object.dataView)); 206 | // console.log(hash(object.float32Array)); 207 | // console.log(hash(object.float64Array)); 208 | // console.log(hash(object.generator)); 209 | // console.log(hash(object.int8Array)); 210 | // console.log(hash(object.int16Array)); 211 | // console.log(hash(object.int32Array)); 212 | // console.log(hash(object.promise)); 213 | // console.log(hash(object.uint8Array)); 214 | // console.log(hash(object.uint8ClampedArray)); 215 | // console.log(hash(object.uint16Array)); 216 | // console.log(hash(object.uint32Array)); 217 | 218 | // console.log(hash(object.ReactStatefulClass)); 219 | // console.log(hash(object.ReactStatefulElement)); 220 | // console.log(hash(object.ReactStatelessClass)); 221 | // console.log(hash(object.ReactStatelessElement)); 222 | // console.log(hash(document.body)); 223 | // console.log(hash(window)); 224 | // }; 225 | 226 | // benchmark(); 227 | // profile(1000); 228 | visualValidation(); 229 | // hashOnlyValidation(); 230 | 231 | const promise = Promise.resolve(123); 232 | 233 | console.log(hash(promise)); 234 | console.log(hash(promise)); 235 | console.log(hash(Promise.resolve(123))); 236 | 237 | const div = document.createElement('div'); 238 | 239 | const root = createRoot(div); 240 | 241 | document.body.appendChild(div); 242 | 243 | root.render(
Check the console for more details!
); 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Plant The Idea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hash-it 2 | 3 | Fast and consistent hashCode for any object type 4 | 5 | ## Table of contents 6 | 7 | - [hash-it](#hash-it) 8 | - [Table of contents](#table-of-contents) 9 | - [Usage](#usage) 10 | - [Overview](#overview) 11 | - [Hash consistency](#hash-consistency) 12 | - [Support](#support) 13 | - [Browsers](#browsers) 14 | - [Node](#node) 15 | - [Development](#development) 16 | 17 | ## Usage 18 | 19 | ```javascript 20 | // ES2015 21 | import hash from 'hash-it'; 22 | 23 | // CommonJS 24 | const hash = require('hash-it'); 25 | 26 | // hash any standard object 27 | console.log(hash({ foo: 'bar' })); // 13729774857125 28 | 29 | // or a circular object 30 | console.log(hash(window)); // 3270731309314 31 | ``` 32 | 33 | ## Overview 34 | 35 | `hash-it` has a simple goal: provide a fast, consistent, unique hashCode for any object type that is uniquely based on its values. This has a number of uses such as duplication prevention, equality comparisons, blockchain construction, etc. 36 | 37 | _Any object type?_ 38 | 39 | Yes, any object type. Primitives, ES2015 classes like `Symbol`, DOM elements (yes, you can even hash the `window` object if you want). Any object type. Here is the list of object classes that produce consistent, unique hashes based on their value: 40 | 41 | - `Arguments` 42 | - `Array` 43 | - `ArrayBuffer` 44 | - `AsyncFunction` (based on `toString`) 45 | - `AsyncGeneratorFunction` (based on `toString`) 46 | - `BigInt` 47 | - `BigInt64Array` 48 | - `BigUint64Array` 49 | - `Boolean` 50 | - `DataView` (based on its `buffer`) 51 | - `Date` (based on `getTime`) 52 | - `DocumentFragment` (based on `outerHTML` of all `children`) 53 | - `Error` (based on `stack`) 54 | - Includes all sub-types (e.g., `TypeError`, `ReferenceError`, etc.) 55 | - `Event` (based on all properties other than `Event.timeStamp`) 56 | - Includes all sub-types (e.g., `MouseEvent`, `KeyboardEvent`, etc.) 57 | - `Float32Array` 58 | - `Float64Array` 59 | - `Function` (based on `toString`) 60 | - `GeneratorFunction` (based on `toString`) 61 | - `Int8Array` 62 | - `Int16Array` 63 | - `Int32Array` 64 | - `HTMLElement` (based on `outerHTML`) 65 | - Includes all sub-types (e.g., `HTMLAnchorElement`, `HTMLDivElement`, etc.) 66 | - `Map` (order-agnostic) 67 | - `Null` 68 | - `Number` 69 | - `Object` (handles circular objects, order-agnostic) 70 | - `Proxy` 71 | - `RegExp` 72 | - `Set` (order-agnostic) 73 | - `SharedArrayBuffer` 74 | - `String` 75 | - `SVGElement` (based on `outerHTML`) 76 | - Includes all sub-types (e.g., `SVGRectElement`, `SVGPolygonElement`, etc.) 77 | - `Symbol` (based on `toString`) 78 | - `Uint8Array` 79 | - `Uint8ClampedArray` 80 | - `Uint16Array` 81 | - `Uint32Array` 82 | - `Undefined` 83 | - `Window` 84 | 85 | _Are there any exceptions?_ 86 | 87 | Sadly, yes, there are a few scenarios where internal values cannot be introspected for the object. In this case, the object is hashed based on its class type and reference. 88 | 89 | - `Promise` 90 | - There is no way to obtain the values contained within due to its asynchronous nature 91 | - `Generator` (the result of calling a `GeneratorFunction`) 92 | - Like `Promise`, there is no way to obtain the values contained within due to its dynamic iterable nature 93 | - `WeakMap` / `WeakRef` / `WeakSet` 94 | - The spec explicitly forbids iteration over them, so the unique values cannot be discovered 95 | 96 | ```ts 97 | const promise = Promise.resolve(123); 98 | 99 | console.log(hash(promise)); // 16843037491939 100 | console.log(hash(promise)); // 16843037491939 101 | console.log(hash(Promise.resolve(123))); // 4622327363876 102 | ``` 103 | 104 | If there is an object class or data type that is missing, please submit an issue. 105 | 106 | ## Hash consistency 107 | 108 | While the hashes will be consistent when calculated within the same environment, there is no guarantee that the resulting hash will be the same across different environments due to environment-specific or browser-specific implementations of features. This is limited to extreme edge cases, such as hashing the `window` object, but should be considered if being used with persistence over different environments. 109 | 110 | ## Support 111 | 112 | ### Browsers 113 | 114 | - Chrome (all versions) 115 | - Firefox (all versions) 116 | - Edge (all versions) 117 | - Opera 15+ 118 | - IE 9+ 119 | - Safari 6+ 120 | - iOS 8+ 121 | - Android 4+ 122 | 123 | ### Node 124 | 125 | - 4+ 126 | 127 | ## Development 128 | 129 | Clone the repo and dependencies via `yarn`. The npm scripts available: 130 | 131 | - `benchmark` => run benchmark of various data types 132 | - `benchmark:compare` => run benchmark of some data types comparing against other hashing modules 133 | - `build` => run rollup to build ESM, CJS, and UMD files 134 | - `clean` => remove files produced from `build` script 135 | - `dev` => run webpack dev server to run example app / playground 136 | - `lint` => run ESLint against all files in the `src` folder 137 | - `lint:fix` => run `lint` script, automatically applying fixable changes 138 | - `prepublishOnly` => run `typecheck`, `lint`, `test`, and `build` 139 | - `start` => alias for `dev` script 140 | - `test` => run jest test functions with `NODE_ENV=test` 141 | - `test:coverage` => run `test` with coverage checker 142 | - `test:watch` => run `test` with persistent watcher 143 | - `typecheck` => run `tsc` to validate internal typings 144 | -------------------------------------------------------------------------------- /__tests__/__helpers__/values.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | export const INTEGER_ARRAY = [1, 2, 3]; 4 | // @ts-expect-error - BigInt values not supported with lower targets. 5 | export const BIG_INTEGER_ARRAY = [21n, 31n]; 6 | 7 | export const ARRAY_BUFFER = new Uint16Array(INTEGER_ARRAY).buffer; 8 | export const DATE = new Date(); 9 | export const ERROR = new Error('boom'); 10 | 11 | const DOCUMENT_FRAGMENT = document.createDocumentFragment(); 12 | DOCUMENT_FRAGMENT.appendChild(document.createElement('div')); 13 | 14 | const EVENT = new Event('custom'); 15 | 16 | export const TEST_VALUES = [ 17 | { 18 | comparator: 'deepEqual', 19 | key: '', 20 | value: {}, 21 | }, 22 | { 23 | comparator: 'deepEqual', 24 | key: 'array', 25 | value: ['foo', 'bar'], 26 | }, 27 | { 28 | comparator: 'deepEqual', 29 | key: 'arguments', 30 | value: (function (_a: string, _b: string) { 31 | return arguments; 32 | })('foo', 'bar'), 33 | }, 34 | { 35 | comparator: 'deepEqual', 36 | key: 'arrayBuffer', 37 | value: ARRAY_BUFFER, 38 | }, 39 | { 40 | comparator: 'is', 41 | key: 'bigInt', 42 | value: BigInt(9007199254740991), 43 | }, 44 | { 45 | comparator: 'is', 46 | key: 'bigInt64Array', 47 | value: new BigInt64Array(BIG_INTEGER_ARRAY), 48 | }, 49 | { 50 | comparator: 'is', 51 | key: 'bigUint64Array', 52 | value: new BigUint64Array(BIG_INTEGER_ARRAY), 53 | }, 54 | { 55 | comparator: 'is', 56 | key: 'boolean', 57 | value: true, 58 | }, 59 | { 60 | comparator: 'deepEqual', 61 | key: 'dataView', 62 | value: new DataView(new ArrayBuffer(2)), 63 | }, 64 | { 65 | comparator: 'is', 66 | key: 'date', 67 | value: DATE, 68 | }, 69 | { 70 | comparator: 'is', 71 | key: 'error', 72 | value: ERROR, 73 | }, 74 | { 75 | comparator: 'deepEqual', 76 | key: 'event', 77 | value: EVENT, 78 | }, 79 | { 80 | comparator: 'is', 81 | key: 'float32Array', 82 | value: new Float32Array(INTEGER_ARRAY), 83 | }, 84 | { 85 | comparator: 'is', 86 | key: 'float64Array', 87 | value: new Float64Array(INTEGER_ARRAY), 88 | }, 89 | { 90 | comparator: 'is', 91 | key: 'function', 92 | // eslint-disable-next-line @typescript-eslint/no-empty-function 93 | value() {}, 94 | }, 95 | { 96 | comparator: 'is', 97 | key: 'generator', 98 | value: (() => { 99 | function* gen() { 100 | yield 1; 101 | yield 2; 102 | yield 3; 103 | } 104 | 105 | return gen(); 106 | })(), 107 | }, 108 | { 109 | comparator: 'is', 110 | key: 'generatorFunction', 111 | // eslint-disable-next-line @typescript-eslint/no-empty-function 112 | *value() {}, 113 | }, 114 | { 115 | comparator: 'deepEqual', 116 | key: 'htmlElement', 117 | value: (() => { 118 | const div = document.createElement('div'); 119 | 120 | div.textContent = 'foo'; 121 | div.className = 'class'; 122 | 123 | return div; 124 | })(), 125 | }, 126 | { 127 | comparator: 'deepEqual', 128 | key: 'svgElement', 129 | value: (() => 130 | document.createElementNS('http://www.w3.org/2000/svg', 'svg'))(), 131 | }, 132 | { 133 | comparator: 'deepEqual', 134 | key: 'documentFragment', 135 | value: DOCUMENT_FRAGMENT, 136 | }, 137 | { 138 | comparator: 'deepEqual', 139 | key: 'int8Array', 140 | value: new Int8Array(INTEGER_ARRAY), 141 | }, 142 | { 143 | comparator: 'deepEqual', 144 | key: 'int16Array', 145 | value: new Int16Array(INTEGER_ARRAY), 146 | }, 147 | { 148 | comparator: 'deepEqual', 149 | key: 'int32Array', 150 | value: new Int32Array(INTEGER_ARRAY), 151 | }, 152 | { 153 | comparator: 'deepEqual', 154 | key: 'map', 155 | value: new Map().set('foo', 'bar'), 156 | }, 157 | { 158 | comparator: 'is', 159 | expectedResult: 'null|null', 160 | expectedString: 'null|null', 161 | key: 'null', 162 | value: null, 163 | }, 164 | { 165 | comparator: 'is', 166 | key: 'number', 167 | value: 12, 168 | }, 169 | { 170 | comparator: 'deepEqual', 171 | key: 'object', 172 | value: { foo: 'bar' }, 173 | }, 174 | { 175 | comparator: 'is', 176 | key: 'promise', 177 | value: Promise.resolve(1), 178 | }, 179 | { 180 | comparator: 'is', 181 | key: 'regexp', 182 | value: /foo/, 183 | }, 184 | { 185 | comparator: 'deepEqual', 186 | key: 'set', 187 | value: new Set().add('foo'), 188 | }, 189 | { 190 | comparator: 'is', 191 | key: 'string', 192 | value: 'foo', 193 | }, 194 | { 195 | comparator: 'is', 196 | key: 'symbol', 197 | value: Symbol('foo'), 198 | }, 199 | { 200 | comparator: 'deepEqual', 201 | key: 'uint8Array', 202 | value: new Uint8Array(INTEGER_ARRAY), 203 | }, 204 | { 205 | comparator: 'deepEqual', 206 | key: 'uint8ClampedArray', 207 | value: new Uint8ClampedArray(INTEGER_ARRAY), 208 | }, 209 | { 210 | comparator: 'deepEqual', 211 | key: 'uint16Array', 212 | value: new Uint16Array(INTEGER_ARRAY), 213 | }, 214 | { 215 | comparator: 'deepEqual', 216 | key: 'uint32Array', 217 | value: new Uint32Array(INTEGER_ARRAY), 218 | }, 219 | { 220 | comparator: 'is', 221 | key: 'undefined', 222 | value: undefined, 223 | }, 224 | { 225 | comparator: 'is', 226 | key: 'weakMap', 227 | value: new WeakMap().set({}, 'foo'), 228 | }, 229 | { 230 | comparator: 'is', 231 | key: 'weakSet', 232 | value: new WeakSet().add({}), 233 | }, 234 | { 235 | comparator: 'is', 236 | key: 'weakRef', 237 | value: new WeakRef({}), 238 | }, 239 | ]; 240 | -------------------------------------------------------------------------------- /__tests__/hash.ts: -------------------------------------------------------------------------------- 1 | import { hash } from '../src/hash'; 2 | 3 | describe('hash', () => { 4 | it('should return the correct value', () => { 5 | const string = 'foo'; 6 | 7 | expect(hash(string)).toBe(794144147649); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import hash from '../src/index'; 2 | 3 | import { TEST_VALUES } from './__helpers__/values'; 4 | import WORDS from './__helpers__/words.json'; 5 | 6 | const VALUES = TEST_VALUES.map(({ key, value }) => ({ key, value })); 7 | const VALUE_HASHES = VALUES.reduce((map, { key, value }) => { 8 | map[key] = hash(value); 9 | 10 | return map; 11 | }, {} as Record); 12 | 13 | const CONSISTENCY_ITERATIONS = 10000; 14 | 15 | describe('hash', () => { 16 | it('should have hashed values that are non-zero', () => { 17 | VALUES.forEach(({ value }) => { 18 | expect(hash(value)).not.toBe(0); 19 | }); 20 | }); 21 | 22 | it('should have a unique hash', () => { 23 | VALUES.forEach(({ key, value }) => { 24 | VALUES.forEach(({ key: otherKey, value: otherValue }) => { 25 | if (value !== otherValue) { 26 | expect( 27 | hash(value), 28 | `{ key: ${key}, otherKey: ${otherKey} }`, 29 | ).not.toBe(hash(otherValue)); 30 | } 31 | }); 32 | }); 33 | }); 34 | 35 | it('should have a consistent hash', () => { 36 | TEST_VALUES.forEach(({ key, value }) => { 37 | for (let index = 0; index < CONSISTENCY_ITERATIONS; ++index) { 38 | expect(hash(value)).toBe(VALUE_HASHES[key]); 39 | } 40 | }); 41 | }); 42 | 43 | it(`should not have collisions with ${WORDS.length} integers`, () => { 44 | const collision: Record = {}; 45 | 46 | let count = 0; 47 | 48 | WORDS.forEach((word: string, index: number) => { 49 | const result = hash(index); 50 | 51 | if (collision[result]) { 52 | count++; 53 | } 54 | 55 | collision[result] = word; 56 | }); 57 | 58 | expect(count).toBe(0); 59 | }); 60 | 61 | it(`should not have collisions with ${WORDS.length} strings`, () => { 62 | const collision: Record = {}; 63 | 64 | let count = 0; 65 | 66 | WORDS.forEach((word: string) => { 67 | const result = hash(word); 68 | 69 | if (collision[result]) { 70 | count++; 71 | } 72 | 73 | collision[result] = word; 74 | }); 75 | 76 | expect(count).toBe(0); 77 | }); 78 | 79 | it('should hash based on a sorted Map', () => { 80 | const map1 = new Map([ 81 | ['foo', 'bar'], 82 | ['bar', 'foo'], 83 | ]); 84 | const map2 = new Map([ 85 | ['bar', 'foo'], 86 | ['foo', 'bar'], 87 | ]); 88 | 89 | expect(hash(map1)).toBe(hash(map2)); 90 | }); 91 | 92 | it('should hash based on a sorted Set', () => { 93 | const set1 = new Set(['foo', 'bar']); 94 | const set2 = new Set(['bar', 'foo']); 95 | 96 | expect(hash(set1)).toBe(hash(set2)); 97 | }); 98 | 99 | it('should hash the same non-enumerable references the same, but unique references differently', () => { 100 | [ 101 | () => 102 | (function* foo() { 103 | yield true; 104 | })(), 105 | () => Promise.resolve(), 106 | () => new WeakMap(), 107 | () => new WeakSet(), 108 | ].forEach((getItem) => { 109 | const item1 = getItem(); 110 | const item2 = getItem(); 111 | 112 | expect(hash(item1)).toBe(hash(item1)); 113 | expect(hash(item1)).not.toBe(hash(item2)); 114 | }); 115 | }); 116 | 117 | it('should support primitive wrappers', () => { 118 | [ 119 | [ 120 | // eslint-disable-next-line @typescript-eslint/no-empty-function 121 | async function () {}, 122 | // eslint-disable-next-line @typescript-eslint/no-empty-function 123 | async function () {}, 124 | async function () { 125 | await console.log('foo'); 126 | }, 127 | ], 128 | [ 129 | // eslint-disable-next-line @typescript-eslint/no-empty-function 130 | async function* () {}, 131 | // eslint-disable-next-line @typescript-eslint/no-empty-function 132 | async function* () {}, 133 | async function* () { 134 | await console.log('foo'); 135 | }, 136 | ], 137 | [ 138 | // eslint-disable-next-line @typescript-eslint/no-empty-function 139 | function () {}, 140 | // eslint-disable-next-line @typescript-eslint/no-empty-function 141 | function () {}, 142 | function () { 143 | console.log('foo'); 144 | }, 145 | ], 146 | [ 147 | // eslint-disable-next-line @typescript-eslint/no-empty-function 148 | function* () {}, 149 | // eslint-disable-next-line @typescript-eslint/no-empty-function 150 | function* () {}, 151 | function* () { 152 | console.log('foo'); 153 | }, 154 | ], 155 | [new Boolean(true), new Boolean(true), new Boolean(false)], 156 | [new Number('123'), new Number('123'), new Number('234')], 157 | [new String('foo'), new String('foo'), new String('bar')], 158 | ].forEach(([item, equalItem, unequalItem]) => { 159 | expect(hash(item)).toBe(hash(equalItem)); 160 | expect(hash(item)).not.toBe(hash(unequalItem)); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /__tests__/stringify.ts: -------------------------------------------------------------------------------- 1 | import { 2 | stringifyArrayBufferFallback, 3 | stringifyArrayBufferModern, 4 | stringifyArrayBufferNone, 5 | } from '../src/stringify'; 6 | import { ARRAY_BUFFER, INTEGER_ARRAY } from './__helpers__/values'; 7 | 8 | describe('ArrayBuffer support', () => { 9 | it('should support modern usage', () => { 10 | const stringified = 'stringified'; 11 | const toString = jest.fn(() => stringified); 12 | 13 | const fromBuffer = Buffer.from; 14 | 15 | const spy = jest.spyOn(Buffer, 'from').mockImplementation((buffer) => { 16 | const result = fromBuffer(buffer); 17 | 18 | result.toString = toString; 19 | 20 | return result; 21 | }); 22 | 23 | const result = stringifyArrayBufferModern(ARRAY_BUFFER); 24 | 25 | expect(spy).toHaveBeenCalledWith(ARRAY_BUFFER); 26 | 27 | spy.mockRestore(); 28 | 29 | expect(result).toBe(stringified); 30 | }); 31 | 32 | it('should support fallback usage', () => { 33 | const stringified = 'stringified'; 34 | 35 | const spy = jest.spyOn(String, 'fromCharCode').mockReturnValue(stringified); 36 | 37 | const result = stringifyArrayBufferFallback(ARRAY_BUFFER); 38 | 39 | expect(spy).toHaveBeenCalledWith(...INTEGER_ARRAY); 40 | 41 | spy.mockRestore(); 42 | 43 | expect(result).toBe(stringified); 44 | }); 45 | 46 | it('should handle no support', () => { 47 | const result = stringifyArrayBufferNone(); 48 | 49 | expect(result).toBe('UNSUPPORTED'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /benchmarks/compare.js: -------------------------------------------------------------------------------- 1 | import Benchmark from 'benchmark'; 2 | import { faker } from '@faker-js/faker'; 3 | import objectHash from 'object-hash'; 4 | import createNodeObjectHash from 'node-object-hash'; 5 | import hashObject from 'hash-object'; 6 | import hash from '../dist/esm/index.mjs'; 7 | 8 | const nodeObjectHash = createNodeObjectHash(); 9 | const suite = new Benchmark.Suite(); 10 | const dataArray = []; 11 | 12 | let dataStairs = { end: 'is near' }; 13 | 14 | console.log('Creating fake data...'); 15 | 16 | for (let i = 0; i < 50; i++) { 17 | dataArray.push({ 18 | name: faker.name.firstName(), 19 | date: new Date(), 20 | address: { 21 | city: faker.address.city(), 22 | streetAddress: faker.address.streetAddress(), 23 | country: faker.address.country(), 24 | }, 25 | email: [ 26 | faker.internet.email(), 27 | faker.internet.email(), 28 | faker.internet.email(), 29 | faker.internet.email(), 30 | ], 31 | randoms: [ 32 | faker.datatype.number(), 33 | faker.random.alphaNumeric(), 34 | faker.datatype.number(), 35 | faker.random.alphaNumeric(), 36 | faker.random.words(), 37 | faker.random.word(), 38 | ], 39 | avatars: [ 40 | { 41 | number: faker.datatype.number(), 42 | avatar: faker.internet.avatar(), 43 | }, 44 | { 45 | number: faker.datatype.number(), 46 | avatar: faker.internet.avatar(), 47 | }, 48 | { 49 | number: faker.datatype.number(), 50 | avatar: faker.internet.avatar(), 51 | }, 52 | { 53 | number: faker.datatype.number(), 54 | avatar: faker.internet.avatar(), 55 | }, 56 | ], 57 | }); 58 | } 59 | 60 | let tmp; 61 | 62 | for (let i = 0; i < 100; i++) { 63 | tmp = { 64 | data: dataStairs, 65 | }; 66 | dataStairs = tmp; 67 | } 68 | 69 | // test preparations 70 | const hashObjectOpts = { algorithm: 'sha256' }; 71 | const objectHashOpts = { 72 | algorithm: 'sha256', 73 | encoding: 'hex', 74 | unorderedArrays: true, 75 | }; 76 | 77 | console.log('Running benchmarks...'); 78 | 79 | // add tests 80 | suite 81 | .add('hash-object', () => { 82 | hashObject(dataStairs, hashObjectOpts); 83 | hashObject(dataArray, hashObjectOpts); 84 | }) 85 | .add('hash-it', () => { 86 | hash(dataStairs); 87 | hash(dataArray); 88 | }) 89 | .add('node-object-hash', () => { 90 | nodeObjectHash.hash(dataStairs); 91 | nodeObjectHash.hash(dataArray); 92 | }) 93 | .add('object-hash', () => { 94 | objectHash(dataStairs, objectHashOpts); 95 | objectHash(dataArray, objectHashOpts); 96 | }) 97 | // add listeners 98 | .on('cycle', (event) => { 99 | console.log(String(event.target)); 100 | }) 101 | .on('complete', function () { 102 | const fastest = this.filter('fastest').map('name'); 103 | 104 | console.log(`Fastest is ${fastest}`); 105 | }) 106 | // run async 107 | .run({ async: true }); 108 | -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { 3 | hashArray, 4 | hashBoolean, 5 | hashCircularObject, 6 | hashDate, 7 | hashError, 8 | hashEvent, 9 | hashFunction, 10 | hashInfinity, 11 | hashMap, 12 | hashNaN, 13 | hashNull, 14 | hashNumber, 15 | hashObject, 16 | hashRegExp, 17 | hashSet, 18 | hashString, 19 | hashUndefined, 20 | } from './tests.js'; 21 | 22 | const REPEATS = [1000, 5000, 10000, 50000, 100000, 500000, 1000000, 5000000]; 23 | const TOTAL = REPEATS.reduce((sum, cycles) => sum + cycles, 0); 24 | 25 | function runTest(name, benchmark) { 26 | let totalTime = 0; 27 | let startTime; 28 | let testTime; 29 | 30 | let displayText = `\n${name}:\n${REPEATS.map((cycles) => { 31 | startTime = Date.now(); 32 | 33 | benchmark(cycles); 34 | 35 | testTime = Date.now() - startTime; 36 | 37 | if (global && global.gc) { 38 | global.gc(); 39 | } 40 | 41 | totalTime += testTime; 42 | 43 | return `${cycles.toLocaleString()}: ${testTime / 1000} sec`; 44 | }).join('\n')}`; 45 | 46 | displayText += `\nAverage: ${Math.round( 47 | (TOTAL / totalTime) * 1000, 48 | ).toLocaleString()} ops/sec`; 49 | 50 | return displayText; 51 | } 52 | 53 | const header = () => 54 | `Benchmark cycles: ${REPEATS.map((cycles) => cycles.toLocaleString()).join( 55 | ' ', 56 | )}`; 57 | 58 | let results = []; 59 | 60 | const logAndSave = (it) => { 61 | results.push(it); 62 | console.log(it); 63 | }; 64 | 65 | console.log(''); 66 | 67 | // header 68 | logAndSave(header()); 69 | 70 | console.log(''); 71 | console.log('Starting benchmarks...'); 72 | console.log(''); 73 | 74 | console.log('Standard value objects:'); 75 | 76 | // primitive tests 77 | logAndSave(runTest('Boolean', hashBoolean)); 78 | logAndSave(runTest('Infinity', hashInfinity)); 79 | logAndSave(runTest('NaN', hashNaN)); 80 | logAndSave(runTest('null', hashNull)); 81 | logAndSave(runTest('Number', hashNumber)); 82 | logAndSave(runTest('String', hashString)); 83 | logAndSave(runTest('undefined', hashUndefined)); 84 | logAndSave(runTest('Function', hashFunction)); 85 | logAndSave(runTest('RegExp', hashRegExp)); 86 | 87 | console.log(''); 88 | console.log('Nested value objects:'); 89 | console.log(''); 90 | 91 | // complex tests 92 | logAndSave(runTest('Array', hashArray)); 93 | logAndSave(runTest('Date', hashDate)); 94 | logAndSave(runTest('Error', hashError)); 95 | logAndSave(runTest('Event', hashEvent)); 96 | logAndSave(runTest('Map', hashMap)); 97 | logAndSave(runTest('Object', hashObject)); 98 | logAndSave(runTest('Object (circular)', hashCircularObject)); 99 | logAndSave(runTest('Set', hashSet)); 100 | 101 | console.log(''); 102 | 103 | // write to file 104 | if (fs && fs.writeFileSync) { 105 | fs.writeFileSync('results_latest.txt', results.join('\n'), 'utf8'); 106 | console.log('Benchmarks done! Results saved to results_latest.txt'); 107 | } else { 108 | console.log('Benchmarks done!'); 109 | } 110 | -------------------------------------------------------------------------------- /benchmarks/tests.js: -------------------------------------------------------------------------------- 1 | import hash from '../dist/esm/index.mjs'; 2 | 3 | function createHashTest(data) { 4 | return function hashTest(cycles) { 5 | let index = -1, 6 | val; 7 | 8 | while (++index < cycles) { 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | val = hash(data); 11 | } 12 | }; 13 | } 14 | 15 | const boolean = true; 16 | const infinite = Infinity; 17 | const notANumber = NaN; 18 | const nul = null; 19 | const number = 123; 20 | const string = 'foo'; 21 | const undef = undefined; 22 | 23 | export const hashBoolean = createHashTest(boolean); 24 | export const hashInfinity = createHashTest(infinite); 25 | export const hashNaN = createHashTest(notANumber); 26 | export const hashNull = createHashTest(nul); 27 | export const hashNumber = createHashTest(number); 28 | export const hashString = createHashTest(string); 29 | export const hashUndefined = createHashTest(undef); 30 | 31 | const array = [boolean, infinite, notANumber, nul, number, string, undef]; 32 | const circularObject = (() => { 33 | function Circular(value) { 34 | this.deeply = { 35 | nested: { 36 | reference: this, 37 | value, 38 | }, 39 | }; 40 | } 41 | 42 | return new Circular(); 43 | })(); 44 | const date = new Date(2000, 0, 1); 45 | const error = new Error('boom'); 46 | const event = new Event('click'); 47 | // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars 48 | const fn = function (_foo, _bar) {}; 49 | const map = new Map([ 50 | ['foo', 'bar'], 51 | ['bar', 'baz'], 52 | ]); 53 | const object = { 54 | boolean, 55 | infinite, 56 | notANumber, 57 | nul, 58 | number, 59 | string, 60 | undef, 61 | }; 62 | const regExp = /foo/g; 63 | const set = new Set(['foo', 'bar', 'baz']); 64 | 65 | export const hashArray = createHashTest(array); 66 | export const hashDate = createHashTest(date); 67 | export const hashError = createHashTest(error); 68 | export const hashEvent = createHashTest(event); 69 | export const hashFunction = createHashTest(fn); 70 | export const hashMap = createHashTest(map); 71 | export const hashObject = createHashTest(object); 72 | export const hashCircularObject = createHashTest(circularObject); 73 | export const hashRegExp = createHashTest(regExp); 74 | export const hashSet = createHashTest(set); 75 | -------------------------------------------------------------------------------- /build/rollup/config.base.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import replace from '@rollup/plugin-replace'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import tsc from 'typescript'; 8 | import { fileURLToPath } from 'url'; 9 | 10 | const ROOT = fileURLToPath(new URL('../..', import.meta.url)); 11 | 12 | export const PACKAGE_JSON = JSON.parse( 13 | fs.readFileSync(path.resolve(ROOT, 'package.json')), 14 | ); 15 | 16 | const external = [ 17 | ...Object.keys(PACKAGE_JSON.dependencies || {}), 18 | ...Object.keys(PACKAGE_JSON.peerDependencies || {}), 19 | ]; 20 | const globals = external.reduce((globals, name) => { 21 | globals[name] = name; 22 | 23 | return globals; 24 | }, {}); 25 | 26 | export const BASE_CONFIG = { 27 | external, 28 | input: path.resolve(ROOT, 'src', 'index.ts'), 29 | output: { 30 | exports: 'named', 31 | globals, 32 | name: 'hash-it', 33 | sourcemap: true, 34 | }, 35 | plugins: [ 36 | replace({ 37 | 'process.env.NODE_ENV': JSON.stringify('production'), 38 | preventAssignment: true, 39 | }), 40 | nodeResolve({ 41 | mainFields: ['module', 'browser', 'main'], 42 | }), 43 | commonjs({ include: /use-sync-external-store/ }), 44 | typescript({ 45 | tsconfig: path.resolve(ROOT, 'build', 'tsconfig', 'base.json'), 46 | typescript: tsc, 47 | }), 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /build/rollup/config.cjs.js: -------------------------------------------------------------------------------- 1 | import { BASE_CONFIG, PACKAGE_JSON } from './config.base.js'; 2 | 3 | export default { 4 | ...BASE_CONFIG, 5 | output: { 6 | ...BASE_CONFIG.output, 7 | file: PACKAGE_JSON.main, 8 | format: 'cjs', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /build/rollup/config.esm.js: -------------------------------------------------------------------------------- 1 | import { BASE_CONFIG, PACKAGE_JSON } from './config.base.js'; 2 | 3 | export default { 4 | ...BASE_CONFIG, 5 | output: { 6 | ...BASE_CONFIG.output, 7 | file: PACKAGE_JSON.module, 8 | format: 'es', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /build/rollup/config.min.js: -------------------------------------------------------------------------------- 1 | import { BASE_CONFIG, PACKAGE_JSON } from './config.base.js'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | ...BASE_CONFIG, 6 | output: { 7 | ...BASE_CONFIG.output, 8 | file: PACKAGE_JSON.browser.replace('umd', 'min'), 9 | format: 'umd', 10 | sourcemap: false, 11 | }, 12 | plugins: [...BASE_CONFIG.plugins, terser()], 13 | }; 14 | -------------------------------------------------------------------------------- /build/rollup/config.umd.js: -------------------------------------------------------------------------------- 1 | import { BASE_CONFIG, PACKAGE_JSON } from './config.base.js'; 2 | 3 | export default { 4 | ...BASE_CONFIG, 5 | output: { 6 | ...BASE_CONFIG.output, 7 | file: PACKAGE_JSON.browser, 8 | format: 'umd', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /build/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "baseUrl": "../../src", 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | "lib": ["DOM", "ESNext"], 8 | "module": "ESNext", 9 | "moduleResolution": "Node", 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "outDir": "../../../dist", 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "strictBindCallApply": true, 23 | "strictNullChecks": true, 24 | "inlineSources": true, 25 | "target": "es5", 26 | "types": ["jest", "node", "react"] 27 | }, 28 | "exclude": ["../../node_modules"], 29 | "include": ["../../src/**/*", "../../__tests__/**/*", "../../DEV_ONLY/**/*"], 30 | "files": ["../../node_modules/jest-expect-message/types/index.d.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /build/tsconfig/cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./declarations.json", 3 | "compilerOptions": { 4 | "declarationDir": "../../dist/cjs/types", 5 | "module": "CommonJS", 6 | "outDir": "../../dist/cjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /build/tsconfig/declarations.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "resolveJsonModule": false 7 | }, 8 | "include": ["../../src/*"] 9 | } 10 | -------------------------------------------------------------------------------- /build/tsconfig/esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./declarations.json", 3 | "compilerOptions": { 4 | "declarationDir": "../../dist/esm/types", 5 | "module": "ESNext", 6 | "outDir": "../../dist/esm" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /build/tsconfig/min.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./declarations.json", 3 | "compilerOptions": { 4 | "declarationDir": "../../dist/min/types", 5 | "module": "UMD", 6 | "outDir": "../../dist/min" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /build/tsconfig/umd.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./declarations.json", 3 | "compilerOptions": { 4 | "declarationDir": "../../dist/umd/types", 5 | "module": "UMD", 6 | "outDir": "../../dist/umd" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | import ESLintWebpackPlugin from 'eslint-webpack-plugin'; 4 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | import webpack from 'webpack'; 8 | 9 | const ROOT = fileURLToPath(new URL('..', import.meta.url)); 10 | const PORT = 3000; 11 | 12 | export default { 13 | cache: true, 14 | 15 | devServer: { 16 | host: 'localhost', 17 | port: PORT, 18 | }, 19 | 20 | devtool: 'source-map', 21 | 22 | entry: [path.resolve(ROOT, 'DEV_ONLY', 'index.tsx')], 23 | 24 | mode: 'development', 25 | 26 | module: { 27 | rules: [ 28 | { 29 | include: [path.resolve(ROOT, 'src'), path.resolve(ROOT, 'DEV_ONLY')], 30 | loader: 'ts-loader', 31 | options: { 32 | reportFiles: ['src/*.{ts|tsx}'], 33 | }, 34 | test: /\.(ts|tsx)$/, 35 | }, 36 | ], 37 | }, 38 | 39 | output: { 40 | filename: 'hash-it.js', 41 | library: 'hashIt', 42 | libraryTarget: 'umd', 43 | path: path.resolve(ROOT, 'dist'), 44 | publicPath: `http://localhost:${PORT}/`, 45 | umdNamedDefine: true, 46 | }, 47 | 48 | plugins: [ 49 | new ESLintWebpackPlugin(), 50 | new webpack.EnvironmentPlugin(['NODE_ENV']), 51 | new HtmlWebpackPlugin(), 52 | ], 53 | 54 | resolve: { 55 | extensions: ['.tsx', '.ts', '.js'], 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export default function hashIt(value: Value): number; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | coveragePathIgnorePatterns: ['node_modules', 'src/types.ts', '__helpers__'], 4 | preset: 'ts-jest', 5 | setupFilesAfterEnv: ['jest-expect-message'], 6 | testEnvironment: 'jsdom', 7 | testPathIgnorePatterns: ['/__tests__/__helpers__', '/benchmark'], 8 | transform: { 9 | // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` 10 | // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` 11 | '^.+\\.tsx?$': [ 12 | 'ts-jest', 13 | { 14 | tsconfig: '/build/tsconfig/base.json' 15 | }, 16 | ], 17 | }, 18 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "planttheidea", 3 | "browser": "dist/umd/index.js", 4 | "browserslist": [ 5 | "defaults", 6 | "Explorer >= 9", 7 | "Safari >= 6", 8 | "Opera >= 15", 9 | "iOS >= 8", 10 | "Android >= 4" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/planttheidea/hash-it/issues" 14 | }, 15 | "description": "Hash any object based on its value", 16 | "devDependencies": { 17 | "@faker-js/faker": "^7.6.0", 18 | "@rollup/plugin-commonjs": "^24.0.0", 19 | "@rollup/plugin-node-resolve": "^15.0.1", 20 | "@rollup/plugin-replace": "^5.0.2", 21 | "@rollup/plugin-terser": "^0.3.0", 22 | "@rollup/plugin-typescript": "^11.0.0", 23 | "@types/jest": "^29.2.5", 24 | "@types/react": "^18.0.26", 25 | "@types/react-dom": "^18.0.10", 26 | "@typescript-eslint/eslint-plugin": "^5.48.0", 27 | "@typescript-eslint/parser": "^5.48.0", 28 | "benchmark": "^2.1.4", 29 | "eslint": "^8.31.0", 30 | "eslint-friendly-formatter": "^4.0.1", 31 | "eslint-webpack-plugin": "^3.2.0", 32 | "hash-object": "^0.1.7", 33 | "html-webpack-plugin": "^5.5.0", 34 | "husky": "^8.0.3", 35 | "jest": "^29.3.1", 36 | "jest-environment-jsdom": "^29.3.1", 37 | "jest-expect-message": "^1.1.3", 38 | "node-object-hash": "^2.3.10", 39 | "object-hash": "^3.0.0", 40 | "ora": "^6.1.2", 41 | "prop-types": "^15.8.1", 42 | "react": "^18.2.0", 43 | "react-dom": "^18.2.0", 44 | "release-it": "^15.6.0", 45 | "rimraf": "^3.0.2", 46 | "rollup": "^3.9.1", 47 | "ts-jest": "^29.0.3", 48 | "ts-loader": "^9.4.2", 49 | "tslib": "^2.4.1", 50 | "typescript": "^4.9.4", 51 | "webpack": "^5.75.0", 52 | "webpack-cli": "^5.0.1", 53 | "webpack-dev-server": "^4.11.1" 54 | }, 55 | "exports": { 56 | ".": { 57 | "import": { 58 | "types": "./dist/esm/types/index.d.ts", 59 | "default": "./dist/esm/index.mjs" 60 | }, 61 | "require": { 62 | "types": "./dist/cjs/types/index.d.ts", 63 | "default": "./dist/cjs/index.cjs" 64 | }, 65 | "default": { 66 | "types": "./dist/umd/types/index.d.ts", 67 | "default": "./dist/umd/index.js" 68 | } 69 | } 70 | }, 71 | "homepage": "https://github.com/planttheidea/hash-it#readme", 72 | "keywords": [ 73 | "hash", 74 | "hashcode", 75 | "object-hash", 76 | "unique" 77 | ], 78 | "license": "MIT", 79 | "main": "dist/cjs/index.cjs", 80 | "module": "dist/esm/index.mjs", 81 | "name": "hash-it", 82 | "repository": { 83 | "type": "git", 84 | "url": "git+https://github.com/planttheidea/hash-it.git" 85 | }, 86 | "resolutions": { 87 | "decompress-response": "^8.0.0" 88 | }, 89 | "scripts": { 90 | "benchmark": "npm run build:esm && node benchmarks/index.js", 91 | "benchmark:compare": "npm run build:esm && node benchmarks/compare.js", 92 | "build": "npm run build:esm && npm run build:cjs && npm run build:umd && npm run build:min", 93 | "build:cjs": "rimraf dist/cjs && NODE_ENV=production rollup -c build/rollup/config.cjs.js && tsc -p ./build/tsconfig/cjs.json", 94 | "build:esm": "rimraf dist/esm && NODE_ENV=production rollup -c build/rollup/config.esm.js && tsc -p ./build/tsconfig/esm.json", 95 | "build:min": "rimraf dist/min && NODE_ENV=production rollup -c build/rollup/config.min.js && tsc -p ./build/tsconfig/min.json", 96 | "build:umd": "rimraf dist/umd && NODE_ENV=production rollup -c build/rollup/config.umd.js && tsc -p ./build/tsconfig/umd.json", 97 | "clean": "rimraf dist", 98 | "dev": "NODE_ENV=development webpack serve --progress --config=build/webpack.config.js", 99 | "lint": "NODE_ENV=test eslint src", 100 | "lint:fix": "npm run lint -- --fix", 101 | "release": "release-it", 102 | "release:beta": "release-it --config=.release-it.beta.json", 103 | "release:scripts": "npm run typecheck && npm run lint && npm run test && npm run build", 104 | "start": "npm run dev", 105 | "test": "NODE_ENV=test jest", 106 | "test:coverage": "npm test -- --coverage", 107 | "test:watch": "npm test -- --watch", 108 | "typecheck": "tsc --noEmit" 109 | }, 110 | "type": "module", 111 | "types": "./index.d.ts", 112 | "version": "6.0.0" 113 | } 114 | -------------------------------------------------------------------------------- /results_2.1.2.txt: -------------------------------------------------------------------------------- 1 | Benchmark cycles: 1,000 5,000 10,000 50,000 100,000 500,000 1,000,000 5,000,000 2 | 3 | Boolean: 4 | 1,000: 0.002 sec 5 | 5,000: 0.006 sec 6 | 10,000: 0.009 sec 7 | 50,000: 0.036 sec 8 | 100,000: 0.07 sec 9 | 500,000: 0.353 sec 10 | 1,000,000: 0.706 sec 11 | 5,000,000: 3.532 sec 12 | Average: 1,414,086 ops/sec 13 | 14 | Infinity: 15 | 1,000: 0 sec 16 | 5,000: 0.002 sec 17 | 10,000: 0.005 sec 18 | 50,000: 0.016 sec 19 | 100,000: 0.03 sec 20 | 500,000: 0.153 sec 21 | 1,000,000: 0.304 sec 22 | 5,000,000: 1.521 sec 23 | Average: 3,282,127 ops/sec 24 | 25 | NaN: 26 | 1,000: 0 sec 27 | 5,000: 0.002 sec 28 | 10,000: 0.004 sec 29 | 50,000: 0.016 sec 30 | 100,000: 0.03 sec 31 | 500,000: 0.152 sec 32 | 1,000,000: 0.305 sec 33 | 5,000,000: 1.52 sec 34 | Average: 3,285,362 ops/sec 35 | 36 | null: 37 | 1,000: 0.001 sec 38 | 5,000: 0.001 sec 39 | 10,000: 0.005 sec 40 | 50,000: 0.015 sec 41 | 100,000: 0.03 sec 42 | 500,000: 0.153 sec 43 | 1,000,000: 0.305 sec 44 | 5,000,000: 1.529 sec 45 | Average: 3,269,250 ops/sec 46 | 47 | Number: 48 | 1,000: 0 sec 49 | 5,000: 0.002 sec 50 | 10,000: 0.004 sec 51 | 50,000: 0.013 sec 52 | 100,000: 0.028 sec 53 | 500,000: 0.137 sec 54 | 1,000,000: 0.271 sec 55 | 5,000,000: 1.359 sec 56 | Average: 3,674,752 ops/sec 57 | 58 | String: 59 | 1,000: 0 sec 60 | 5,000: 0 sec 61 | 10,000: 0.002 sec 62 | 50,000: 0.003 sec 63 | 100,000: 0.006 sec 64 | 500,000: 0.029 sec 65 | 1,000,000: 0.059 sec 66 | 5,000,000: 0.293 sec 67 | Average: 17,005,102 ops/sec 68 | 69 | undefined: 70 | 1,000: 0 sec 71 | 5,000: 0.003 sec 72 | 10,000: 0.005 sec 73 | 50,000: 0.022 sec 74 | 100,000: 0.044 sec 75 | 500,000: 0.22 sec 76 | 1,000,000: 0.441 sec 77 | 5,000,000: 2.201 sec 78 | Average: 2,270,436 ops/sec 79 | 80 | Function: 81 | 1,000: 0.002 sec 82 | 5,000: 0.007 sec 83 | 10,000: 0.012 sec 84 | 50,000: 0.056 sec 85 | 100,000: 0.111 sec 86 | 500,000: 0.556 sec 87 | 1,000,000: 1.117 sec 88 | 5,000,000: 5.535 sec 89 | Average: 901,298 ops/sec 90 | 91 | RegExp: 92 | 1,000: 0.003 sec 93 | 5,000: 0.007 sec 94 | 10,000: 0.016 sec 95 | 50,000: 0.071 sec 96 | 100,000: 0.142 sec 97 | 500,000: 0.706 sec 98 | 1,000,000: 1.42 sec 99 | 5,000,000: 7.074 sec 100 | Average: 706,219 ops/sec 101 | 102 | Array: 103 | 1,000: 0.007 sec 104 | 5,000: 0.031 sec 105 | 10,000: 0.058 sec 106 | 50,000: 0.284 sec 107 | 100,000: 0.57 sec 108 | 500,000: 2.838 sec 109 | 1,000,000: 5.668 sec 110 | 5,000,000: 28.313 sec 111 | Average: 176,494 ops/sec 112 | 113 | Map: 114 | 1,000: 0.011 sec 115 | 5,000: 0.046 sec 116 | 10,000: 0.092 sec 117 | 50,000: 0.449 sec 118 | 100,000: 0.901 sec 119 | 500,000: 4.498 sec 120 | 1,000,000: 8.996 sec 121 | 5,000,000: 44.969 sec 122 | Average: 111,170 ops/sec 123 | 124 | Object: 125 | 1,000: 0.011 sec 126 | 5,000: 0.049 sec 127 | 10,000: 0.098 sec 128 | 50,000: 0.48 sec 129 | 100,000: 0.957 sec 130 | 500,000: 4.797 sec 131 | 1,000,000: 9.58 sec 132 | 5,000,000: 47.898 sec 133 | Average: 104,368 ops/sec 134 | 135 | Object (recursive): 136 | 1,000: 0.006 sec 137 | 5,000: 0.022 sec 138 | 10,000: 0.045 sec 139 | 50,000: 0.214 sec 140 | 100,000: 0.43 sec 141 | 500,000: 2.155 sec 142 | 1,000,000: 4.321 sec 143 | 5,000,000: 21.637 sec 144 | Average: 231,217 ops/sec 145 | 146 | Set: 147 | 1,000: 0.013 sec 148 | 5,000: 0.06 sec 149 | 10,000: 0.119 sec 150 | 50,000: 0.59 sec 151 | 100,000: 1.179 sec 152 | 500,000: 5.937 sec 153 | 1,000,000: 11.895 sec 154 | 5,000,000: 58.968 sec 155 | Average: 84,636 ops/sec -------------------------------------------------------------------------------- /results_3.0.0.txt: -------------------------------------------------------------------------------- 1 | Benchmark cycles: 1,000 5,000 10,000 50,000 100,000 500,000 1,000,000 5,000,000 2 | 3 | Boolean: 4 | 1,000: 0.002 sec 5 | 5,000: 0.005 sec 6 | 10,000: 0.006 sec 7 | 50,000: 0.014 sec 8 | 100,000: 0.015 sec 9 | 500,000: 0.078 sec 10 | 1,000,000: 0.156 sec 11 | 5,000,000: 0.774 sec 12 | Average: 6,348,571 ops/sec 13 | 14 | Infinity: 15 | 1,000: 0 sec 16 | 5,000: 0.003 sec 17 | 10,000: 0.009 sec 18 | 50,000: 0.028 sec 19 | 100,000: 0.057 sec 20 | 500,000: 0.244 sec 21 | 1,000,000: 0.482 sec 22 | 5,000,000: 2.531 sec 23 | Average: 1,987,478 ops/sec 24 | 25 | NaN: 26 | 1,000: 0.001 sec 27 | 5,000: 0.003 sec 28 | 10,000: 0.009 sec 29 | 50,000: 0.032 sec 30 | 100,000: 0.049 sec 31 | 500,000: 0.246 sec 32 | 1,000,000: 0.499 sec 33 | 5,000,000: 2.442 sec 34 | Average: 2,031,698 ops/sec 35 | 36 | null: 37 | 1,000: 0.001 sec 38 | 5,000: 0.002 sec 39 | 10,000: 0.003 sec 40 | 50,000: 0.013 sec 41 | 100,000: 0.016 sec 42 | 500,000: 0.077 sec 43 | 1,000,000: 0.154 sec 44 | 5,000,000: 0.808 sec 45 | Average: 6,206,704 ops/sec 46 | 47 | Number: 48 | 1,000: 0.001 sec 49 | 5,000: 0.002 sec 50 | 10,000: 0.007 sec 51 | 50,000: 0.026 sec 52 | 100,000: 0.048 sec 53 | 500,000: 0.239 sec 54 | 1,000,000: 0.48 sec 55 | 5,000,000: 2.46 sec 56 | Average: 2,042,905 ops/sec 57 | 58 | String: 59 | 1,000: 0.001 sec 60 | 5,000: 0 sec 61 | 10,000: 0.001 sec 62 | 50,000: 0.007 sec 63 | 100,000: 0.003 sec 64 | 500,000: 0.015 sec 65 | 1,000,000: 0.031 sec 66 | 5,000,000: 0.148 sec 67 | Average: 32,359,223 ops/sec 68 | 69 | undefined: 70 | 1,000: 0.001 sec 71 | 5,000: 0.001 sec 72 | 10,000: 0.004 sec 73 | 50,000: 0.015 sec 74 | 100,000: 0.03 sec 75 | 500,000: 0.159 sec 76 | 1,000,000: 0.282 sec 77 | 5,000,000: 1.508 sec 78 | Average: 3,333,000 ops/sec 79 | 80 | Function: 81 | 1,000: 0.001 sec 82 | 5,000: 0.004 sec 83 | 10,000: 0.012 sec 84 | 50,000: 0.039 sec 85 | 100,000: 0.074 sec 86 | 500,000: 0.357 sec 87 | 1,000,000: 0.737 sec 88 | 5,000,000: 3.327 sec 89 | Average: 1,464,733 ops/sec 90 | 91 | RegExp: 92 | 1,000: 0.001 sec 93 | 5,000: 0.004 sec 94 | 10,000: 0.013 sec 95 | 50,000: 0.038 sec 96 | 100,000: 0.072 sec 97 | 500,000: 0.363 sec 98 | 99 | 1,000,000: 0.731 sec 100 | 5,000,000: 3.728 sec 101 | Average: 1,346,667 ops/sec 102 | 103 | Array: 104 | 1,000: 0.006 sec 105 | 5,000: 0.018 sec 106 | 10,000: 0.041 sec 107 | 50,000: 0.175 sec 108 | 100,000: 0.296 sec 109 | 500,000: 1.497 sec 110 | 1,000,000: 2.956 sec 111 | 5,000,000: 15.282 sec 112 | Average: 328,844 ops/sec 113 | 114 | Map: 115 | 1,000: 0.006 sec 116 | 5,000: 0.021 sec 117 | 10,000: 0.043 sec 118 | 50,000: 0.16 sec 119 | 100,000: 0.328 sec 120 | 500,000: 1.468 sec 121 | 1,000,000: 2.929 sec 122 | 5,000,000: 15.159 sec 123 | Average: 331,411 ops/sec 124 | 125 | Object: 126 | 1,000: 0.005 sec 127 | 5,000: 0.019 sec 128 | 10,000: 0.044 sec 129 | 50,000: 0.181 sec 130 | 100,000: 0.366 sec 131 | 500,000: 1.854 sec 132 | 1,000,000: 3.641 sec 133 | 5,000,000: 18.252 sec 134 | Average: 273,623 ops/sec 135 | 136 | Object (circular): 137 | 1,000: 0.002 sec 138 | 5,000: 0.011 sec 139 | 10,000: 0.019 sec 140 | 50,000: 0.07 sec 141 | 100,000: 0.135 sec 142 | 500,000: 0.698 sec 143 | 1,000,000: 1.369 sec 144 | 5,000,000: 7.074 sec 145 | Average: 710,813 ops/sec 146 | 147 | Set: 148 | 1,000: 0.004 sec 149 | 5,000: 0.019 sec 150 | 10,000: 0.044 sec 151 | 50,000: 0.187 sec 152 | 100,000: 0.373 sec 153 | 500,000: 1.877 sec 154 | 1,000,000: 3.858 sec 155 | 5,000,000: 18.738 sec 156 | Average: 265,578 ops/sec 157 | -------------------------------------------------------------------------------- /results_4.1.0.txt: -------------------------------------------------------------------------------- 1 | Benchmark cycles: 1,000 5,000 10,000 50,000 100,000 500,000 1,000,000 5,000,000 2 | 3 | Boolean: 4 | 1,000: 0.002 sec 5 | 5,000: 0.003 sec 6 | 10,000: 0.004 sec 7 | 50,000: 0.005 sec 8 | 100,000: 0.007 sec 9 | 500,000: 0.034 sec 10 | 1,000,000: 0.062 sec 11 | 5,000,000: 0.312 sec 12 | Average: 15,538,462 ops/sec 13 | 14 | Infinity: 15 | 1,000: 0.001 sec 16 | 5,000: 0.002 sec 17 | 10,000: 0.004 sec 18 | 50,000: 0.007 sec 19 | 100,000: 0.013 sec 20 | 500,000: 0.064 sec 21 | 1,000,000: 0.122 sec 22 | 5,000,000: 0.612 sec 23 | Average: 8,080,000 ops/sec 24 | 25 | NaN: 26 | 1,000: 0.001 sec 27 | 5,000: 0 sec 28 | 10,000: 0.003 sec 29 | 50,000: 0.005 sec 30 | 100,000: 0.007 sec 31 | 500,000: 0.037 sec 32 | 1,000,000: 0.069 sec 33 | 5,000,000: 0.347 sec 34 | Average: 14,213,220 ops/sec 35 | 36 | null: 37 | 1,000: 0 sec 38 | 5,000: 0.001 sec 39 | 10,000: 0.001 sec 40 | 50,000: 0.003 sec 41 | 100,000: 0.006 sec 42 | 500,000: 0.031 sec 43 | 1,000,000: 0.058 sec 44 | 5,000,000: 0.287 sec 45 | Average: 17,224,806 ops/sec 46 | 47 | Number: 48 | 1,000: 0 sec 49 | 5,000: 0.001 sec 50 | 10,000: 0.001 sec 51 | 50,000: 0.005 sec 52 | 100,000: 0.007 sec 53 | 500,000: 0.035 sec 54 | 1,000,000: 0.065 sec 55 | 5,000,000: 0.326 sec 56 | Average: 15,150,000 ops/sec 57 | 58 | String: 59 | 1,000: 0 sec 60 | 5,000: 0 sec 61 | 10,000: 0.003 sec 62 | 50,000: 0.004 sec 63 | 100,000: 0.006 sec 64 | 500,000: 0.03 sec 65 | 1,000,000: 0.056 sec 66 | 5,000,000: 0.278 sec 67 | Average: 17,681,698 ops/sec 68 | 69 | undefined: 70 | 1,000: 0 sec 71 | 5,000: 0.001 sec 72 | 10,000: 0.003 sec 73 | 50,000: 0.01 sec 74 | 100,000: 0.015 sec 75 | 500,000: 0.078 sec 76 | 1,000,000: 0.152 sec 77 | 5,000,000: 0.756 sec 78 | Average: 6,567,488 ops/sec 79 | 80 | Function: 81 | 1,000: 0.001 sec 82 | 5,000: 0.002 sec 83 | 10,000: 0.005 sec 84 | 50,000: 0.016 sec 85 | 100,000: 0.034 sec 86 | 500,000: 0.157 sec 87 | 1,000,000: 0.314 sec 88 | 5,000,000: 1.573 sec 89 | Average: 3,171,265 ops/sec 90 | 91 | RegExp: 92 | 1,000: 0.001 sec 93 | 5,000: 0.003 sec 94 | 10,000: 0.008 sec 95 | 50,000: 0.023 sec 96 | 100,000: 0.044 sec 97 | 500,000: 0.22 sec 98 | 1,000,000: 0.439 sec 99 | 5,000,000: 2.196 sec 100 | Average: 2,271,984 ops/sec 101 | 102 | Array: 103 | 1,000: 0.005 sec 104 | 5,000: 0.017 sec 105 | 10,000: 0.034 sec 106 | 50,000: 0.141 sec 107 | 100,000: 0.282 sec 108 | 500,000: 1.416 sec 109 | 1,000,000: 2.849 sec 110 | 5,000,000: 14.519 sec 111 | Average: 346,052 ops/sec 112 | 113 | Map: 114 | 1,000: 0.004 sec 115 | 5,000: 0.013 sec 116 | 10,000: 0.024 sec 117 | 50,000: 0.084 sec 118 | 100,000: 0.169 sec 119 | 500,000: 0.837 sec 120 | 1,000,000: 1.664 sec 121 | 5,000,000: 8.27 sec 122 | Average: 602,440 ops/sec 123 | 124 | Object: 125 | 1,000: 0.007 sec 126 | 5,000: 0.02 sec 127 | 10,000: 0.041 sec 128 | 50,000: 0.172 sec 129 | 100,000: 0.362 sec 130 | 500,000: 1.717 sec 131 | 1,000,000: 3.431 sec 132 | 5,000,000: 17.187 sec 133 | Average: 290,622 ops/sec 134 | 135 | Object (circular): 136 | 1,000: 0.004 sec 137 | 5,000: 0.014 sec 138 | 10,000: 0.031 sec 139 | 50,000: 0.126 sec 140 | 100,000: 0.249 sec 141 | 500,000: 1.253 sec 142 | 1,000,000: 2.512 sec 143 | 5,000,000: 12.591 sec 144 | Average: 397,259 ops/sec 145 | 146 | Set: 147 | 1,000: 0.003 sec 148 | 5,000: 0.011 sec 149 | 10,000: 0.022 sec 150 | 50,000: 0.081 sec 151 | 100,000: 0.157 sec 152 | 500,000: 0.799 sec 153 | 1,000,000: 1.558 sec 154 | 5,000,000: 7.934 sec 155 | Average: 630,951 ops/sec -------------------------------------------------------------------------------- /results_5.0.2.txt: -------------------------------------------------------------------------------- 1 | Benchmark cycles: 1,000 5,000 10,000 50,000 100,000 500,000 1,000,000 5,000,000 2 | 3 | Boolean: 4 | 1,000: 0.002 sec 5 | 5,000: 0.003 sec 6 | 10,000: 0.003 sec 7 | 50,000: 0.005 sec 8 | 100,000: 0.007 sec 9 | 500,000: 0.027 sec 10 | 1,000,000: 0.048 sec 11 | 5,000,000: 0.235 sec 12 | Average: 20,200,000 ops/sec 13 | 14 | Infinity: 15 | 1,000: 0.001 sec 16 | 5,000: 0.001 sec 17 | 10,000: 0.002 sec 18 | 50,000: 0.006 sec 19 | 100,000: 0.013 sec 20 | 500,000: 0.056 sec 21 | 1,000,000: 0.101 sec 22 | 5,000,000: 0.508 sec 23 | Average: 9,688,953 ops/sec 24 | 25 | NaN: 26 | 1,000: 0 sec 27 | 5,000: 0 sec 28 | 10,000: 0.001 sec 29 | 50,000: 0.005 sec 30 | 100,000: 0.005 sec 31 | 500,000: 0.029 sec 32 | 1,000,000: 0.05 sec 33 | 5,000,000: 0.238 sec 34 | Average: 20,323,171 ops/sec 35 | 36 | null: 37 | 1,000: 0 sec 38 | 5,000: 0.001 sec 39 | 10,000: 0.001 sec 40 | 50,000: 0.005 sec 41 | 100,000: 0.008 sec 42 | 500,000: 0.028 sec 43 | 1,000,000: 0.047 sec 44 | 5,000,000: 0.227 sec 45 | Average: 21,028,391 ops/sec 46 | 47 | Number: 48 | 1,000: 0 sec 49 | 5,000: 0.001 sec 50 | 10,000: 0.002 sec 51 | 50,000: 0.004 sec 52 | 100,000: 0.005 sec 53 | 500,000: 0.029 sec 54 | 1,000,000: 0.05 sec 55 | 5,000,000: 0.231 sec 56 | Average: 20,701,863 ops/sec 57 | 58 | String: 59 | 1,000: 0 sec 60 | 5,000: 0 sec 61 | 10,000: 0.001 sec 62 | 50,000: 0.006 sec 63 | 100,000: 0.005 sec 64 | 500,000: 0.03 sec 65 | 1,000,000: 0.067 sec 66 | 5,000,000: 0.254 sec 67 | Average: 18,363,636 ops/sec 68 | 69 | undefined: 70 | 1,000: 0 sec 71 | 5,000: 0.001 sec 72 | 10,000: 0.004 sec 73 | 50,000: 0.009 sec 74 | 100,000: 0.013 sec 75 | 500,000: 0.061 sec 76 | 1,000,000: 0.116 sec 77 | 5,000,000: 0.586 sec 78 | Average: 8,437,975 ops/sec 79 | 80 | Function: 81 | 1,000: 0.001 sec 82 | 5,000: 0.001 sec 83 | 10,000: 0.005 sec 84 | 50,000: 0.016 sec 85 | 100,000: 0.028 sec 86 | 500,000: 0.131 sec 87 | 1,000,000: 0.258 sec 88 | 5,000,000: 1.284 sec 89 | Average: 3,866,589 ops/sec 90 | 91 | RegExp: 92 | 1,000: 0.001 sec 93 | 5,000: 0.003 sec 94 | 10,000: 0.007 sec 95 | 50,000: 0.018 sec 96 | 100,000: 0.033 sec 97 | 500,000: 0.152 sec 98 | 1,000,000: 0.307 sec 99 | 5,000,000: 1.529 sec 100 | Average: 3,251,707 ops/sec 101 | 102 | Array: 103 | 1,000: 0.005 sec 104 | 5,000: 0.017 sec 105 | 10,000: 0.034 sec 106 | 50,000: 0.134 sec 107 | 100,000: 0.27 sec 108 | 500,000: 1.33 sec 109 | 1,000,000: 2.635 sec 110 | 5,000,000: 12.868 sec 111 | Average: 385,474 ops/sec 112 | 113 | Map: 114 | 1,000: 0.003 sec 115 | 5,000: 0.014 sec 116 | 10,000: 0.023 sec 117 | 50,000: 0.082 sec 118 | 100,000: 0.159 sec 119 | 500,000: 0.8 sec 120 | 1,000,000: 1.599 sec 121 | 5,000,000: 8.169 sec 122 | Average: 614,435 ops/sec 123 | 124 | Object: 125 | 1,000: 0.007 sec 126 | 5,000: 0.021 sec 127 | 10,000: 0.038 sec 128 | 50,000: 0.149 sec 129 | 100,000: 0.304 sec 130 | 500,000: 1.49 sec 131 | 1,000,000: 3.043 sec 132 | 5,000,000: 14.881 sec 133 | Average: 334,420 ops/sec 134 | 135 | Object (circular): 136 | 1,000: 0.004 sec 137 | 5,000: 0.014 sec 138 | 10,000: 0.03 sec 139 | 50,000: 0.109 sec 140 | 100,000: 0.21 sec 141 | 500,000: 1.052 sec 142 | 1,000,000: 2.089 sec 143 | 5,000,000: 10.427 sec 144 | Average: 478,364 ops/sec 145 | 146 | Set: 147 | 1,000: 0.002 sec 148 | 5,000: 0.011 sec 149 | 10,000: 0.022 sec 150 | 50,000: 0.079 sec 151 | 100,000: 0.149 sec 152 | 500,000: 0.738 sec 153 | 1,000,000: 1.481 sec 154 | 5,000,000: 7.357 sec 155 | Average: 677,508 ops/sec -------------------------------------------------------------------------------- /results_latest.txt: -------------------------------------------------------------------------------- 1 | Benchmark cycles: 1,000 5,000 10,000 50,000 100,000 500,000 1,000,000 5,000,000 2 | 3 | Boolean: 4 | 1,000: 0 sec 5 | 5,000: 0.002 sec 6 | 10,000: 0.001 sec 7 | 50,000: 0.002 sec 8 | 100,000: 0.002 sec 9 | 500,000: 0.008 sec 10 | 1,000,000: 0.016 sec 11 | 5,000,000: 0.082 sec 12 | Average: 58,991,150 ops/sec 13 | 14 | Infinity: 15 | 1,000: 0 sec 16 | 5,000: 0.001 sec 17 | 10,000: 0.001 sec 18 | 50,000: 0.003 sec 19 | 100,000: 0.003 sec 20 | 500,000: 0.016 sec 21 | 1,000,000: 0.03 sec 22 | 5,000,000: 0.151 sec 23 | Average: 32,517,073 ops/sec 24 | 25 | NaN: 26 | 1,000: 0 sec 27 | 5,000: 0 sec 28 | 10,000: 0.001 sec 29 | 50,000: 0.001 sec 30 | 100,000: 0.002 sec 31 | 500,000: 0.01 sec 32 | 1,000,000: 0.021 sec 33 | 5,000,000: 0.104 sec 34 | Average: 47,956,835 ops/sec 35 | 36 | null: 37 | 1,000: 0.001 sec 38 | 5,000: 0 sec 39 | 10,000: 0.001 sec 40 | 50,000: 0.003 sec 41 | 100,000: 0.002 sec 42 | 500,000: 0.01 sec 43 | 1,000,000: 0.021 sec 44 | 5,000,000: 0.103 sec 45 | Average: 47,276,596 ops/sec 46 | 47 | Number: 48 | 1,000: 0 sec 49 | 5,000: 0 sec 50 | 10,000: 0 sec 51 | 50,000: 0.001 sec 52 | 100,000: 0.002 sec 53 | 500,000: 0.01 sec 54 | 1,000,000: 0.02 sec 55 | 5,000,000: 0.1 sec 56 | Average: 50,120,301 ops/sec 57 | 58 | String: 59 | 1,000: 0 sec 60 | 5,000: 0.001 sec 61 | 10,000: 0.001 sec 62 | 50,000: 0.002 sec 63 | 100,000: 0.003 sec 64 | 500,000: 0.01 sec 65 | 1,000,000: 0.02 sec 66 | 5,000,000: 0.103 sec 67 | Average: 47,614,286 ops/sec 68 | 69 | undefined: 70 | 1,000: 0 sec 71 | 5,000: 0 sec 72 | 10,000: 0.001 sec 73 | 50,000: 0.001 sec 74 | 100,000: 0.004 sec 75 | 500,000: 0.016 sec 76 | 1,000,000: 0.033 sec 77 | 5,000,000: 0.163 sec 78 | Average: 30,577,982 ops/sec 79 | 80 | Function: 81 | 1,000: 0 sec 82 | 5,000: 0.001 sec 83 | 10,000: 0.002 sec 84 | 50,000: 0.007 sec 85 | 100,000: 0.015 sec 86 | 500,000: 0.073 sec 87 | 1,000,000: 0.146 sec 88 | 5,000,000: 0.728 sec 89 | Average: 6,858,025 ops/sec 90 | 91 | RegExp: 92 | 1,000: 0.001 sec 93 | 5,000: 0.002 sec 94 | 10,000: 0.006 sec 95 | 50,000: 0.015 sec 96 | 100,000: 0.03 sec 97 | 500,000: 0.146 sec 98 | 1,000,000: 0.294 sec 99 | 5,000,000: 1.468 sec 100 | Average: 3,397,554 ops/sec 101 | 102 | Array: 103 | 1,000: 0.003 sec 104 | 5,000: 0.005 sec 105 | 10,000: 0.012 sec 106 | 50,000: 0.036 sec 107 | 100,000: 0.074 sec 108 | 500,000: 0.367 sec 109 | 1,000,000: 0.699 sec 110 | 5,000,000: 3.331 sec 111 | Average: 1,472,498 ops/sec 112 | 113 | Date: 114 | 1,000: 0 sec 115 | 5,000: 0.001 sec 116 | 10,000: 0.003 sec 117 | 50,000: 0.01 sec 118 | 100,000: 0.014 sec 119 | 500,000: 0.072 sec 120 | 1,000,000: 0.144 sec 121 | 5,000,000: 0.718 sec 122 | Average: 6,929,314 ops/sec 123 | 124 | Error: 125 | 1,000: 0.001 sec 126 | 5,000: 0.005 sec 127 | 10,000: 0.013 sec 128 | 50,000: 0.049 sec 129 | 100,000: 0.097 sec 130 | 500,000: 0.483 sec 131 | 1,000,000: 0.966 sec 132 | 5,000,000: 4.899 sec 133 | Average: 1,023,491 ops/sec 134 | 135 | Event: 136 | 1,000: 0.002 sec 137 | 5,000: 0.006 sec 138 | 10,000: 0.008 sec 139 | 50,000: 0.03 sec 140 | 100,000: 0.059 sec 141 | 500,000: 0.291 sec 142 | 1,000,000: 0.582 sec 143 | 5,000,000: 2.913 sec 144 | Average: 1,713,184 ops/sec 145 | 146 | Map: 147 | 1,000: 0.001 sec 148 | 5,000: 0.006 sec 149 | 10,000: 0.006 sec 150 | 50,000: 0.03 sec 151 | 100,000: 0.06 sec 152 | 500,000: 0.298 sec 153 | 1,000,000: 0.596 sec 154 | 5,000,000: 2.983 sec 155 | Average: 1,674,874 ops/sec 156 | 157 | Object: 158 | 1,000: 0.003 sec 159 | 5,000: 0.007 sec 160 | 10,000: 0.011 sec 161 | 50,000: 0.055 sec 162 | 100,000: 0.105 sec 163 | 500,000: 0.525 sec 164 | 1,000,000: 1.051 sec 165 | 5,000,000: 5.246 sec 166 | Average: 951,878 ops/sec 167 | 168 | Object (circular): 169 | 1,000: 0.002 sec 170 | 5,000: 0.007 sec 171 | 10,000: 0.01 sec 172 | 50,000: 0.052 sec 173 | 100,000: 0.1 sec 174 | 500,000: 0.497 sec 175 | 1,000,000: 0.994 sec 176 | 5,000,000: 4.984 sec 177 | Average: 1,003,009 ops/sec 178 | 179 | Set: 180 | 1,000: 0.001 sec 181 | 5,000: 0.006 sec 182 | 10,000: 0.007 sec 183 | 50,000: 0.027 sec 184 | 100,000: 0.056 sec 185 | 500,000: 0.275 sec 186 | 1,000,000: 0.549 sec 187 | 5,000,000: 2.749 sec 188 | Average: 1,816,349 ops/sec -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import { SEPARATOR } from './constants'; 2 | import { namespaceComplexValue } from './utils'; 3 | 4 | import type { Class } from './constants'; 5 | 6 | export const NON_ENUMERABLE_CLASS_CACHE = new WeakMap< 7 | NonEnumerableObject, 8 | string 9 | >(); 10 | 11 | type NonEnumerableObject = 12 | | Generator 13 | | Promise 14 | | WeakMap 15 | | WeakSet; 16 | 17 | let refId = 0; 18 | export function getUnsupportedHash( 19 | value: NonEnumerableObject, 20 | classType: Class, 21 | ): string { 22 | const cached = NON_ENUMERABLE_CLASS_CACHE.get(value); 23 | 24 | if (cached) { 25 | return cached; 26 | } 27 | 28 | const toCache = namespaceComplexValue( 29 | classType, 30 | 'NOT_ENUMERABLE' + SEPARATOR + refId++, 31 | ); 32 | 33 | NON_ENUMERABLE_CLASS_CACHE.set(value, toCache); 34 | 35 | return toCache; 36 | } 37 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SEPARATOR = '|'; 2 | export const XML_ELEMENT_REGEXP = /\[object ([HTML|SVG](.*)Element)\]/; 3 | 4 | export const CLASSES = { 5 | '[object Arguments]': 0, 6 | '[object Array]': 1, 7 | '[object ArrayBuffer]': 2, 8 | '[object AsyncFunction]': 3, 9 | '[object AsyncGeneratorFunction]': 4, 10 | '[object BigInt]': 5, 11 | '[object BigInt64Array]': 6, 12 | '[object BigUint64Array]': 7, 13 | '[object Boolean]': 8, 14 | '[object DataView]': 9, 15 | '[object Date]': 10, 16 | '[object DocumentFragment]': 11, 17 | '[object Error]': 12, 18 | '[object Event]': 13, 19 | '[object Float32Array]': 14, 20 | '[object Float64Array]': 15, 21 | '[object Function]': 16, 22 | '[object Generator]': 17, 23 | '[object GeneratorFunction]': 18, 24 | '[object Int8Array]': 19, 25 | '[object Int16Array]': 20, 26 | '[object Map]': 21, 27 | '[object Number]': 22, 28 | '[object Object]': 23, 29 | '[object Promise]': 24, 30 | '[object RegExp]': 25, 31 | '[object Set]': 26, 32 | '[object SharedArrayBuffer]': 27, 33 | '[object String]': 28, 34 | '[object Uint8Array]': 29, 35 | '[object Uint8ClampedArray]': 30, 36 | '[object Uint16Array]': 31, 37 | '[object Uint32Array]': 32, 38 | '[object WeakMap]': 33, 39 | '[object WeakRef]': 34, 40 | '[object WeakSet]': 35, 41 | CUSTOM: 36, 42 | ELEMENT: 37, 43 | } as const; 44 | 45 | export type Class = keyof typeof CLASSES; 46 | 47 | export const ARRAY_LIKE_CLASSES = { 48 | '[object Arguments]': 1, 49 | '[object Array]': 2, 50 | } as const; 51 | 52 | export type ArrayLikeClass = keyof typeof ARRAY_LIKE_CLASSES; 53 | 54 | export const NON_ENUMERABLE_CLASSES = { 55 | '[object Generator]': 1, 56 | '[object Promise]': 2, 57 | '[object WeakMap]': 3, 58 | '[object WeakRef]': 4, 59 | '[object WeakSet]': 5, 60 | } as const; 61 | 62 | export type NonEnumerableClass = keyof typeof NON_ENUMERABLE_CLASSES; 63 | 64 | export const PRIMITIVE_WRAPPER_CLASSES = { 65 | '[object AsyncFunction]': 1, 66 | '[object AsyncGeneratorFunction]': 2, 67 | '[object Boolean]': 3, 68 | '[object Function]': 4, 69 | '[object GeneratorFunction]': 5, 70 | '[object Number]': 6, 71 | '[object String]': 7, 72 | } as const; 73 | 74 | export type PrimitiveWrapperClass = keyof typeof PRIMITIVE_WRAPPER_CLASSES; 75 | 76 | export const TYPED_ARRAY_CLASSES = { 77 | '[object BigInt64Array]': 1, 78 | '[object BigUint64Array]': 2, 79 | '[object Float32Array]': 3, 80 | '[object Float64Array]': 4, 81 | '[object Int8Array]': 5, 82 | '[object Int16Array]': 6, 83 | '[object Uint8Array]': 7, 84 | '[object Uint8ClampedArray]': 8, 85 | '[object Uint16Array]': 9, 86 | '[object Uint32Array]': 10, 87 | } as const; 88 | 89 | export type TypedArrayClass = keyof typeof TYPED_ARRAY_CLASSES; 90 | 91 | export const RECURSIVE_CLASSES = { 92 | '[object Arguments]': 1, 93 | '[object Array]': 2, 94 | '[object ArrayBuffer]': 3, 95 | '[object BigInt64Array]': 4, 96 | '[object BigUint64Array]': 5, 97 | '[object DataView]': 6, 98 | '[object Float32Array]': 7, 99 | '[object Float64Array]': 8, 100 | '[object Int8Array]': 9, 101 | '[object Int16Array]': 10, 102 | '[object Map]': 11, 103 | '[object Object]': 12, 104 | '[object Set]': 13, 105 | '[object SharedArrayBuffer]': 14, 106 | '[object Uint8Array]': 15, 107 | '[object Uint8ClampedArray]': 16, 108 | '[object Uint16Array]': 17, 109 | '[object Uint32Array]': 18, 110 | CUSTOM: 19, 111 | } as const; 112 | 113 | export type RecursiveClass = keyof typeof RECURSIVE_CLASSES; 114 | 115 | export const HASHABLE_TYPES = { 116 | bigint: 'i', 117 | boolean: 'b', 118 | empty: 'e', 119 | function: 'g', 120 | number: 'n', 121 | object: 'o', 122 | string: 's', 123 | symbol: 's', 124 | } as const; 125 | 126 | export type HashableType = keyof typeof HASHABLE_TYPES; 127 | -------------------------------------------------------------------------------- /src/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * based on string passed, get the integer hash value 3 | * through bitwise operation (based on spinoff of dbj2 4 | * with enhancements for reduced collisions) 5 | */ 6 | export function hash(string: string): number { 7 | let index = string.length; 8 | let hashA = 5381; 9 | let hashB = 52711; 10 | let charCode; 11 | 12 | while (index--) { 13 | charCode = string.charCodeAt(index); 14 | 15 | hashA = (hashA * 33) ^ charCode; 16 | hashB = (hashB * 33) ^ charCode; 17 | } 18 | 19 | return (hashA >>> 0) * 4096 + (hashB >>> 0); 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { hash } from './hash'; 2 | import { stringify } from './stringify'; 3 | 4 | export default function hashIt(value: Value): number { 5 | return hash(stringify(value, undefined)); 6 | } 7 | -------------------------------------------------------------------------------- /src/sort.ts: -------------------------------------------------------------------------------- 1 | export function sortByKey( 2 | first: [string, string], 3 | second: [string, string], 4 | ): boolean { 5 | return first[0] > second[0]; 6 | } 7 | 8 | export function sortBySelf(first: string, second: string) { 9 | return first > second; 10 | } 11 | 12 | export function sort( 13 | array: any[], 14 | fn: (item: any, comparisonItem: any) => boolean, 15 | ) { 16 | let subIndex; 17 | let value; 18 | 19 | for (let index = 0; index < array.length; ++index) { 20 | value = array[index]; 21 | 22 | for ( 23 | subIndex = index - 1; 24 | ~subIndex && fn(array[subIndex], value); 25 | --subIndex 26 | ) { 27 | array[subIndex + 1] = array[subIndex]; 28 | } 29 | 30 | array[subIndex + 1] = value; 31 | } 32 | 33 | return array; 34 | } 35 | -------------------------------------------------------------------------------- /src/stringify.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ARRAY_LIKE_CLASSES, 3 | HASHABLE_TYPES, 4 | NON_ENUMERABLE_CLASSES, 5 | PrimitiveWrapperClass, 6 | PRIMITIVE_WRAPPER_CLASSES, 7 | RECURSIVE_CLASSES, 8 | SEPARATOR, 9 | TYPED_ARRAY_CLASSES, 10 | XML_ELEMENT_REGEXP, 11 | } from './constants'; 12 | import { sort, sortByKey, sortBySelf } from './sort'; 13 | 14 | import type { 15 | ArrayLikeClass, 16 | Class, 17 | NonEnumerableClass, 18 | RecursiveClass, 19 | TypedArrayClass, 20 | } from './constants'; 21 | import { getUnsupportedHash } from './cache'; 22 | import { namespaceComplexValue } from './utils'; 23 | 24 | interface RecursiveState { 25 | cache: WeakMap; 26 | id: number; 27 | } 28 | 29 | const toString = Object.prototype.toString; 30 | 31 | function stringifyComplexType( 32 | value: any, 33 | classType: Class, 34 | state: RecursiveState, 35 | ) { 36 | if (RECURSIVE_CLASSES[classType as RecursiveClass]) { 37 | return stringifyRecursiveAsJson(classType as RecursiveClass, value, state); 38 | } 39 | 40 | if (classType === '[object Date]') { 41 | return namespaceComplexValue(classType, value.getTime()); 42 | } 43 | 44 | if (classType === '[object RegExp]') { 45 | return namespaceComplexValue(classType, value.toString()); 46 | } 47 | 48 | if (classType === '[object Event]') { 49 | return namespaceComplexValue( 50 | classType, 51 | [ 52 | value.bubbles, 53 | value.cancelBubble, 54 | value.cancelable, 55 | value.composed, 56 | value.currentTarget, 57 | value.defaultPrevented, 58 | value.eventPhase, 59 | value.isTrusted, 60 | value.returnValue, 61 | value.target, 62 | value.type, 63 | ].join(), 64 | ); 65 | } 66 | 67 | if (classType === '[object Error]') { 68 | return namespaceComplexValue( 69 | classType, 70 | value.message + SEPARATOR + value.stack, 71 | ); 72 | } 73 | 74 | if (classType === '[object DocumentFragment]') { 75 | return namespaceComplexValue(classType, stringifyDocumentFragment(value)); 76 | } 77 | 78 | const element = classType.match(XML_ELEMENT_REGEXP); 79 | 80 | if (element) { 81 | return namespaceComplexValue( 82 | 'ELEMENT', 83 | element[1] + SEPARATOR + value.outerHTML, 84 | ); 85 | } 86 | 87 | if (NON_ENUMERABLE_CLASSES[classType as NonEnumerableClass]) { 88 | return getUnsupportedHash(value, classType); 89 | } 90 | 91 | if (PRIMITIVE_WRAPPER_CLASSES[classType as PrimitiveWrapperClass]) { 92 | return namespaceComplexValue(classType, value.toString()); 93 | } 94 | 95 | // This would only be hit with custom `toStringTag` values 96 | return stringifyRecursiveAsJson('CUSTOM', value, state); 97 | } 98 | 99 | function stringifyRecursiveAsJson( 100 | classType: RecursiveClass, 101 | value: any, 102 | state: RecursiveState, 103 | ) { 104 | const cached = state.cache.get(value); 105 | 106 | if (cached) { 107 | return namespaceComplexValue(classType, 'RECURSIVE~' + cached); 108 | } 109 | 110 | state.cache.set(value, ++state.id); 111 | 112 | if (classType === '[object Object]') { 113 | return value[Symbol.iterator] 114 | ? getUnsupportedHash(value, classType) 115 | : namespaceComplexValue(classType, stringifyObject(value, state)); 116 | } 117 | 118 | if (ARRAY_LIKE_CLASSES[classType as ArrayLikeClass]) { 119 | return namespaceComplexValue(classType, stringifyArray(value, state)); 120 | } 121 | 122 | if (classType === '[object Map]') { 123 | return namespaceComplexValue(classType, stringifyMap(value, state)); 124 | } 125 | 126 | if (classType === '[object Set]') { 127 | return namespaceComplexValue(classType, stringifySet(value, state)); 128 | } 129 | 130 | if (TYPED_ARRAY_CLASSES[classType as TypedArrayClass]) { 131 | return namespaceComplexValue(classType, value.join()); 132 | } 133 | 134 | if (classType === '[object ArrayBuffer]') { 135 | return namespaceComplexValue(classType, stringifyArrayBuffer(value)); 136 | } 137 | 138 | if (classType === '[object DataView]') { 139 | return namespaceComplexValue(classType, stringifyArrayBuffer(value.buffer)); 140 | } 141 | 142 | if (NON_ENUMERABLE_CLASSES[classType as NonEnumerableClass]) { 143 | return getUnsupportedHash(value, classType); 144 | } 145 | 146 | return namespaceComplexValue('CUSTOM', stringifyObject(value, state)); 147 | } 148 | 149 | export function stringifyArray(value: any[], state: RecursiveState) { 150 | let index = value.length; 151 | 152 | const result: string[] = new Array(index); 153 | 154 | while (--index >= 0) { 155 | result[index] = stringify(value[index], state); 156 | } 157 | 158 | return result.join(); 159 | } 160 | 161 | export function stringifyArrayBufferModern(buffer: ArrayBufferLike): string { 162 | return Buffer.from(buffer).toString('utf8'); 163 | } 164 | 165 | export function stringifyArrayBufferFallback(buffer: ArrayBufferLike): string { 166 | return String.fromCharCode.apply( 167 | null, 168 | new Uint16Array(buffer) as unknown as number[], 169 | ); 170 | } 171 | 172 | export function stringifyArrayBufferNone(): string { 173 | return 'UNSUPPORTED'; 174 | } 175 | 176 | export function stringifyDocumentFragment(fragment: DocumentFragment): string { 177 | const children = fragment.children; 178 | 179 | let index = children.length; 180 | 181 | const innerHTML: string[] = new Array(index); 182 | 183 | while (--index >= 0) { 184 | innerHTML[index] = children[index]!.outerHTML; 185 | } 186 | 187 | return innerHTML.join(); 188 | } 189 | 190 | const stringifyArrayBuffer = 191 | typeof Buffer !== 'undefined' && typeof Buffer.from === 'function' 192 | ? stringifyArrayBufferModern 193 | : typeof Uint16Array === 'function' 194 | ? stringifyArrayBufferFallback 195 | : stringifyArrayBufferNone; 196 | 197 | export function stringifyMap(map: Map, state: RecursiveState) { 198 | const result: string[] | Array<[string, string]> = new Array(map.size); 199 | 200 | let index = 0; 201 | map.forEach((value, key) => { 202 | result[index++] = [stringify(key, state), stringify(value, state)]; 203 | }); 204 | 205 | sort(result, sortByKey); 206 | 207 | while (--index >= 0) { 208 | result[index] = '[' + result[index]![0] + ',' + result[index]![1] + ']'; 209 | } 210 | 211 | return '[' + result.join() + ']'; 212 | } 213 | 214 | export function stringifyObject( 215 | value: Record, 216 | state: RecursiveState, 217 | ) { 218 | const properties = sort(Object.getOwnPropertyNames(value), sortBySelf); 219 | const length = properties.length; 220 | const result: string[] = new Array(length); 221 | 222 | let index = length; 223 | 224 | while (--index >= 0) { 225 | result[index] = 226 | properties[index]! + ':' + stringify(value[properties[index]!], state); 227 | } 228 | 229 | return '{' + result.join() + '}'; 230 | } 231 | 232 | export function stringifySet(set: Set, state: RecursiveState) { 233 | const result: string[] = new Array(set.size); 234 | 235 | let index = 0; 236 | set.forEach((value) => { 237 | result[index++] = stringify(value, state); 238 | }); 239 | 240 | return '[' + sort(result, sortBySelf).join() + ']'; 241 | } 242 | 243 | export function stringify( 244 | value: any, 245 | state: RecursiveState | undefined, 246 | ): string { 247 | const type = typeof value; 248 | 249 | if (value == null || type === 'undefined') { 250 | return HASHABLE_TYPES.empty + value; 251 | } 252 | 253 | if (type === 'object') { 254 | return stringifyComplexType( 255 | value, 256 | toString.call(value) as unknown as Class, 257 | state || { cache: new WeakMap(), id: 1 }, 258 | ); 259 | } 260 | 261 | if (type === 'function' || type === 'symbol') { 262 | return HASHABLE_TYPES[type] + value.toString(); 263 | } 264 | 265 | if (type === 'boolean') { 266 | return HASHABLE_TYPES.boolean + +value; 267 | } 268 | 269 | return HASHABLE_TYPES[type] + value; 270 | } 271 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { CLASSES, HASHABLE_TYPES, SEPARATOR } from './constants'; 2 | 3 | import type { Class } from './constants'; 4 | 5 | export function namespaceComplexValue( 6 | classType: Class, 7 | value: string | number | boolean, 8 | ) { 9 | return ( 10 | HASHABLE_TYPES.object + SEPARATOR + CLASSES[classType] + SEPARATOR + value 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build/tsconfig/base.json" 3 | } 4 | --------------------------------------------------------------------------------