@verifyhash/deep-equal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 verifyhash
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.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @verifyhash/deep-equal
2
+
3
+ Zero-dependency structural deep equality for Node.js. One function, one
4
+ boolean, and a precisely documented rule for every awkward case: `NaN`,
5
+ `±0`, sparse arrays, `Map`/`Set` with object keys, typed arrays, boxed
6
+ primitives, prototype mismatches — and cyclic structures, which terminate
7
+ instead of blowing the stack.
8
+
9
+ Who it's for: anyone writing tests, cache-invalidation checks, or
10
+ change-detection who is tired of `JSON.stringify(a) === JSON.stringify(b)`
11
+ (which breaks on key order, `undefined`, `NaN`, `Map`, `Set`, cycles, and
12
+ typed arrays — all of which this handles correctly).
13
+
14
+ ## Install
15
+
16
+ ```
17
+ npm install @verifyhash/deep-equal
18
+ ```
19
+
20
+ Zero runtime dependencies. CommonJS, works on any maintained Node.js.
21
+ TypeScript declarations ship in the package (`index.d.ts`).
22
+
23
+ ## Example
24
+
25
+ ```js
26
+ const { deepEqual } = require('@verifyhash/deep-equal');
27
+
28
+ deepEqual({ a: 1, b: [NaN] }, { b: [NaN], a: 1 }); // true (key order irrelevant, NaN === NaN)
29
+ deepEqual(new Map([[{ id: 1 }, 'x']]),
30
+ new Map([[{ id: 1 }, 'x']])); // true (object keys match structurally)
31
+ deepEqual(new Set([NaN, 1]), new Set([1, NaN])); // true
32
+ deepEqual(0, -0); // false (+0 and -0 are distinct)
33
+ deepEqual([, 1], [undefined, 1]); // false (a hole is not an explicit undefined)
34
+ deepEqual(new Float64Array(1), new Uint8Array(8)); // false (same bytes, different type)
35
+
36
+ // Cycles terminate and compare correctly:
37
+ const a = { name: 'node' }; a.self = a;
38
+ const b = { name: 'node' }; b.self = b;
39
+ deepEqual(a, b); // true
40
+ ```
41
+
42
+ ## API
43
+
44
+ ### `deepEqual(a, b) => boolean`
45
+
46
+ Returns `true` iff `a` and `b` are structurally equal under these rules:
47
+
48
+ | Category | Rule |
49
+ |---|---|
50
+ | Primitives | `Object.is` semantics: **`NaN` equals `NaN`** (unlike `===`), and **`+0` does NOT equal `-0`** (unlike `===`). Chosen because a deep-equal that says `NaN !== NaN` is useless in tests, and collapsing `±0` silently hides real sign bugs (`1/x` flips infinity sign). |
51
+ | `null` / `undefined` | Distinct from each other and from everything else. `{ a: undefined }` does NOT equal `{}`. |
52
+ | Plain objects | Own **enumerable string keys** only; key order irrelevant. **Symbol keys and non-enumerable properties are ignored** — two objects differing only in symbol-keyed props are equal. |
53
+ | Prototypes | Both sides must share the **same prototype object** (identity). A plain object never equals a class instance with the same fields; `Object.create(null)` objects only equal other null-proto objects. This is the strict rule Node's `assert.deepStrictEqual` family uses; it keeps `[]` vs `{}` and `Map` vs `Set` false for free. |
54
+ | Arrays | Length + elements. Sparse **holes are preserved**: `[, 1]` equals `[, 1]` but not `[undefined, 1]`. Extra own enumerable props on an array are compared too. |
55
+ | `Date` | By `getTime()`. Two Invalid Dates (both `NaN`) are equal. |
56
+ | `RegExp` | By `source` and `flags`. `lastIndex` is ignored. |
57
+ | `Map` | Equal `size`, and every `[key, value]` entry matches an unused entry of the other map — **keys and values both compared structurally**, so `{id:1}` keys match across maps. Insertion order irrelevant. |
58
+ | `Set` | Equal `size`, every member structurally matches an unused member. `NaN` in a Set works (Sets use SameValueZero internally). Insertion order irrelevant. |
59
+ | `ArrayBuffer` / TypedArrays / `DataView` | **Byte-wise**, and only against the *same* type: a `Float64Array` never equals a `Uint8Array` even over identical bytes. A `subarray()` view compares only its own window, not the whole backing buffer. |
60
+ | Boxed primitives | `new Number(3)` equals `new Number(3)` (unwrapped via `valueOf`, `Object.is` semantics) but never equals primitive `3` — object vs primitive is a type mismatch. |
61
+ | Functions | Reference equality only. Two distinct closures with identical source are not equal. |
62
+ | Cycles | Self-referential and mutually-referential structures terminate via an in-progress pair memo (WeakMap). Equality is **coinductive**: two cycles are equal when their infinite unrollings agree — so a 1-cycle `a→a` equals a 2-cycle `b→b'→b` when every payload matches. A cyclic structure never equals an acyclic one of different shape. |
63
+
64
+ ### Honest limits
65
+
66
+ - **Symbol-keyed and non-enumerable properties are invisible** to the
67
+ comparison, by design. If those carry meaning for you, this is the wrong
68
+ tool.
69
+ - Map/Set matching of *object* keys/members uses a **greedy** scan, not a full
70
+ bipartite matcher. With multiple structurally-equal-but-distinct candidate
71
+ keys mapping to different values, a pathological arrangement could greedily
72
+ mis-pair and report `false` where a smarter matcher finds a pairing. Sizes
73
+ and normal data never hit this.
74
+ - Object-keyed `Map`/`Set` comparison is **O(n·m)** in the worst case (every
75
+ key of one map scanned against the other). Fine for the hundreds of entries
76
+ typical in tests/config; do not diff two million-entry object-keyed maps
77
+ with it.
78
+ - Getters are **invoked** (values are read as a consumer would see them), and
79
+ cross-realm objects (e.g. from `node:vm`) fail the prototype-identity rule.
80
+ - Floats in typed arrays compare **byte-wise**: `Float64Array [0]` vs `[-0]`
81
+ differ (different bit patterns), and two `NaN`s with different bit payloads
82
+ would differ — plain-number `NaN`s (outside typed arrays) always compare
83
+ equal.
84
+
85
+ ## Running the tests
86
+
87
+ ```
88
+ cd deep-equal
89
+ node test/index.test.js
90
+ ```
91
+
92
+ 231 deterministic golden-vector checks (every table row is asserted in both
93
+ argument orders), no timers, no network, exit code 0 on success.
94
+
95
+ ## License
96
+
97
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Type declarations for @verifyhash/deep-equal — zero-dependency structural
3
+ * equality with Map/Set/Date/RegExp/typed-array/cycle support.
4
+ *
5
+ * Hand-written against index.js — every runtime export is declared here.
6
+ */
7
+
8
+ /**
9
+ * Structural deep equality.
10
+ *
11
+ * Rules (documented in the README):
12
+ * - Primitives compare with Object.is semantics: `NaN` equals `NaN`,
13
+ * `+0` does NOT equal `-0`.
14
+ * - `null` and `undefined` are distinct.
15
+ * - Plain objects: key order irrelevant; only own enumerable STRING keys
16
+ * are compared (symbol keys and non-enumerable properties are ignored).
17
+ * - Objects must share the same prototype (identity) — a plain object never
18
+ * equals a class instance.
19
+ * - Arrays: length + elements; a sparse hole is NOT an explicit `undefined`.
20
+ * - `Date` by `getTime()` (two Invalid Dates are equal), `RegExp` by
21
+ * source + flags.
22
+ * - `Map` / `Set`: equal size and structurally matching entries/members
23
+ * (object keys and members match structurally, not by identity).
24
+ * - `ArrayBuffer`, every TypedArray, and `DataView` compare byte-wise, and
25
+ * only against the same type.
26
+ * - Cyclic (self- or mutually-referential) structures terminate and compare
27
+ * correctly.
28
+ * - Functions are equal only by reference.
29
+ *
30
+ * @param a - first value
31
+ * @param b - second value
32
+ * @returns `true` iff `a` and `b` are structurally equal under the rules above.
33
+ */
34
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
package/index.js ADDED
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @verifyhash/deep-equal — zero-dependency structural equality for Node.js.
5
+ *
6
+ * deepEqual(a, b) -> boolean. Handles primitives (NaN equals NaN; +0 and -0
7
+ * are DIFFERENT), plain objects (key order irrelevant, symbol keys ignored),
8
+ * arrays (sparse holes are not the same as explicit undefined), Date, RegExp,
9
+ * Map, Set, ArrayBuffer / every TypedArray / DataView (byte-wise), boxed
10
+ * primitives, and cyclic structures (self- and mutually-referential graphs
11
+ * terminate and compare correctly via an in-progress pair memo).
12
+ *
13
+ * Documented rules (see README for the full rationale):
14
+ * - Numbers compare with Object.is semantics: NaN === NaN is TRUE, +0 vs -0
15
+ * is FALSE.
16
+ * - Objects must share the SAME prototype (identity): a plain object never
17
+ * equals a class instance, even with identical own properties.
18
+ * - Only own ENUMERABLE STRING keys are compared; symbol-keyed and
19
+ * non-enumerable properties are ignored.
20
+ * - Functions are equal only by reference.
21
+ * - Typed arrays / DataView / ArrayBuffer compare BYTE-WISE and only against
22
+ * the same view type (Float64Array never equals a Uint8Array, even over
23
+ * identical bytes).
24
+ */
25
+
26
+ const toString = Object.prototype.toString;
27
+ const hasOwn = Object.prototype.hasOwnProperty;
28
+
29
+ /** Byte-wise comparison of two Uint8Array views of equal length. */
30
+ function eqBytes(a, b) {
31
+ if (a.length !== b.length) return false;
32
+ for (let i = 0; i < a.length; i++) {
33
+ if (a[i] !== b[i]) return false;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ /**
39
+ * Own enumerable string-keyed properties: same key set, structurally equal
40
+ * values. Symbol keys and non-enumerable properties are deliberately ignored.
41
+ */
42
+ function eqOwnKeys(a, b, seen) {
43
+ const keysA = Object.keys(a);
44
+ const keysB = Object.keys(b);
45
+ if (keysA.length !== keysB.length) return false;
46
+ // Membership must be checked against b's ENUMERABLE keys, not hasOwnProperty,
47
+ // so a non-enumerable own property on b can never mask a real mismatch.
48
+ const setB = new Set(keysB);
49
+ for (const k of keysA) {
50
+ if (!setB.has(k)) return false;
51
+ if (!eq(a[k], b[k], seen)) return false;
52
+ }
53
+ return true;
54
+ }
55
+
56
+ /**
57
+ * Maps: equal size, and every [key, value] entry of `a` matches an unused
58
+ * entry of `b` — keys and values both compared STRUCTURALLY. Uses a fast path
59
+ * through b.get() for SameValueZero-identical keys, then a greedy scan for
60
+ * structural key matches (object keys).
61
+ */
62
+ function eqMap(a, b, seen) {
63
+ if (a.size !== b.size) return false;
64
+ const usedB = new Set();
65
+ for (const [ka, va] of a) {
66
+ // Fast path: b has an identical (SameValueZero) key with an equal value.
67
+ if (b.has(ka) && !usedB.has(ka) && eq(va, b.get(ka), seen)) {
68
+ usedB.add(ka);
69
+ continue;
70
+ }
71
+ // Structural path: find any unused key of b that deep-equals ka with a
72
+ // deep-equal value (needed for object keys, which differ by identity).
73
+ let found = false;
74
+ for (const [kb, vb] of b) {
75
+ if (usedB.has(kb)) continue;
76
+ if (eq(ka, kb, seen) && eq(va, vb, seen)) {
77
+ usedB.add(kb);
78
+ found = true;
79
+ break;
80
+ }
81
+ }
82
+ if (!found) return false;
83
+ }
84
+ return true;
85
+ }
86
+
87
+ /**
88
+ * Sets: equal size, and every member of `a` structurally matches an unused
89
+ * member of `b`. Fast path via b.has() (SameValueZero — this is what makes
90
+ * NaN-in-a-Set work directly), then a greedy structural scan for objects.
91
+ */
92
+ function eqSet(a, b, seen) {
93
+ if (a.size !== b.size) return false;
94
+ const usedB = new Set();
95
+ for (const va of a) {
96
+ if (b.has(va) && !usedB.has(va)) {
97
+ usedB.add(va);
98
+ continue;
99
+ }
100
+ let found = false;
101
+ for (const vb of b) {
102
+ if (usedB.has(vb)) continue;
103
+ if (eq(va, vb, seen)) {
104
+ usedB.add(vb);
105
+ found = true;
106
+ break;
107
+ }
108
+ }
109
+ if (!found) return false;
110
+ }
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Core recursive comparison. `seen` is a WeakMap<object, WeakSet<object>> of
116
+ * IN-PROGRESS (a, b) pairs: when a cycle re-enters a pair already being
117
+ * compared, we return true (coinductive equality — the cycle itself cannot
118
+ * introduce a difference; any real difference is found elsewhere in the
119
+ * graph). Pairs are removed when their comparison completes, so a `false`
120
+ * verdict is never wrongly memoized as `true` for later sibling comparisons.
121
+ */
122
+ function eq(a, b, seen) {
123
+ // Object.is: identical references, identical primitives, NaN === NaN true,
124
+ // +0 vs -0 false — exactly the documented primitive semantics.
125
+ if (Object.is(a, b)) return true;
126
+ if (
127
+ a === null || b === null ||
128
+ typeof a !== 'object' || typeof b !== 'object'
129
+ ) {
130
+ // Covers null/undefined distinction, primitive vs object (a boxed Number
131
+ // never equals its primitive), and functions (reference-only equality —
132
+ // already handled by Object.is above).
133
+ return false;
134
+ }
135
+
136
+ // Documented prototype rule: both sides must share the SAME prototype
137
+ // object. Plain object vs class instance -> false; subclassed Array vs
138
+ // plain Array -> false; also separates [] from {} and Map from Set.
139
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
140
+
141
+ const tag = toString.call(a);
142
+ if (tag !== toString.call(b)) return false; // belt-and-braces cross-type guard
143
+
144
+ switch (tag) {
145
+ case '[object Date]':
146
+ // Object.is so two Invalid Dates (getTime() = NaN) compare equal.
147
+ return Object.is(a.getTime(), b.getTime());
148
+ case '[object RegExp]':
149
+ return a.source === b.source && a.flags === b.flags;
150
+ case '[object Number]':
151
+ case '[object String]':
152
+ case '[object Boolean]':
153
+ case '[object BigInt]':
154
+ case '[object Symbol]':
155
+ // Boxed primitives: unwrap and compare with Object.is (so
156
+ // new Number(NaN) equals new Number(NaN), new Number(0) vs
157
+ // new Number(-0) is false). Extra expando props are ignored.
158
+ return Object.is(a.valueOf(), b.valueOf());
159
+ case '[object ArrayBuffer]':
160
+ return eqBytes(new Uint8Array(a), new Uint8Array(b));
161
+ default:
162
+ break;
163
+ }
164
+
165
+ if (ArrayBuffer.isView(a)) {
166
+ // Same view type guaranteed by the prototype + tag checks above, so
167
+ // Float64Array vs Uint8Array over identical bytes is already false here.
168
+ // DataView and every TypedArray flavor compare byte-wise.
169
+ if (a.byteLength !== b.byteLength) return false;
170
+ return eqBytes(
171
+ new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
172
+ new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
173
+ );
174
+ }
175
+
176
+ // --- Containers that can participate in cycles: memo the in-progress pair.
177
+ let pairs = seen.get(a);
178
+ if (pairs !== undefined && pairs.has(b)) return true; // cycle re-entry
179
+ if (pairs === undefined) {
180
+ pairs = new WeakSet();
181
+ seen.set(a, pairs);
182
+ }
183
+ pairs.add(b);
184
+
185
+ let result;
186
+ if (tag === '[object Map]') {
187
+ result = eqMap(a, b, seen);
188
+ } else if (tag === '[object Set]') {
189
+ result = eqSet(a, b, seen);
190
+ } else if (Array.isArray(a)) {
191
+ // length compared explicitly (it is not an enumerable key); sparse holes
192
+ // then fall out of the key comparison: a hole has no own key, an explicit
193
+ // undefined does — so [ ,1 ] never equals [ undefined, 1 ].
194
+ result = a.length === b.length && eqOwnKeys(a, b, seen);
195
+ } else {
196
+ // Plain objects and same-class instances (prototype identity was already
197
+ // required above).
198
+ result = eqOwnKeys(a, b, seen);
199
+ }
200
+
201
+ pairs.delete(b);
202
+ return result;
203
+ }
204
+
205
+ /**
206
+ * Structural deep equality.
207
+ * @param {*} a
208
+ * @param {*} b
209
+ * @returns {boolean} true iff a and b are structurally equal under the
210
+ * documented rules.
211
+ */
212
+ function deepEqual(a, b) {
213
+ return eq(a, b, new WeakMap());
214
+ }
215
+
216
+ module.exports = { deepEqual };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@verifyhash/deep-equal",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency structural deep equality for Node.js: plain objects, arrays (sparse-hole aware), Date, RegExp, Map/Set with structural object keys, ArrayBuffer/TypedArray/DataView (byte-wise), boxed primitives, NaN-equals-NaN with +0/-0 distinction, and cyclic structures — fully typed from birth.",
5
+ "keywords": [
6
+ "deep-equal",
7
+ "deep-equality",
8
+ "structural-equality",
9
+ "equal",
10
+ "compare",
11
+ "cycles",
12
+ "circular",
13
+ "map",
14
+ "set",
15
+ "typed-array",
16
+ "zero-dependency",
17
+ "typescript"
18
+ ],
19
+ "main": "index.js",
20
+ "types": "index.d.ts",
21
+ "scripts": {
22
+ "test": "node test/index.test.js"
23
+ },
24
+ "license": "MIT",
25
+ "files": [
26
+ "index.js",
27
+ "README.md",
28
+ "LICENSE",
29
+ "index.d.ts"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/verifyhash/libs.git",
37
+ "directory": "deep-equal"
38
+ },
39
+ "homepage": "https://github.com/verifyhash/libs/tree/main/deep-equal#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/verifyhash/libs/issues"
42
+ }
43
+ }