mirror: A fast canonicalisation utility for stable object equality
0

Configure Feed

Select the types of activity you want to include in your feed.

fix: prevent stack overflow on self-referential toJSON

Marais Rossouw (Jun 30, 2026, 8:57 PM +1000) caed78f9 2b0b4743

+75 -11
+63
lib/mod.test.ts
··· 133 133 assertEquals(identify(o), identify({ a: 1 })); 134 134 }); 135 135 136 + Deno.test('toJSON :: result cycling back to the original does not overflow', () => { 137 + const fresh: any = {}; 138 + fresh.toJSON = () => ({ wrap: fresh }); 139 + 140 + const stable: any = {}; 141 + const wrapper = { wrap: stable }; 142 + stable.toJSON = () => wrapper; 143 + 144 + assertEquals(identify(fresh), identify(fresh)); 145 + assertEquals(identify(fresh), identify(stable)); 146 + }); 147 + 148 + Deno.test('toJSON :: result resolving to an ancestor closes the cycle', () => { 149 + const getter: any = { 150 + get g() { 151 + return { toJSON: () => getter }; 152 + }, 153 + }; 154 + assert(identify(getter)); 155 + assertEquals(identify(getter), identify(getter)); 156 + 157 + const parent: any = {}; 158 + const child: any = { toJSON: () => parent }; 159 + parent.child = child; 160 + assert(identify(parent)); 161 + assertEquals(identify(parent), identify(parent)); 162 + 163 + const root: any = {}; 164 + const mid: any = {}; 165 + root.mid = mid; 166 + mid.leaf = { toJSON: () => root }; 167 + assert(identify(root)); 168 + assertEquals(identify(root), identify(root)); 169 + }); 170 + 171 + Deno.test('toJSON :: ancestor cycles resolve through every container', () => { 172 + const viaSet: any = { s: new Set() }; 173 + viaSet.s.add({ toJSON: () => viaSet }); 174 + 175 + const viaMap: any = { m: new Map() }; 176 + viaMap.m.set('k', { toJSON: () => viaMap }); 177 + 178 + const viaArray: any = { arr: [] as unknown[] }; 179 + viaArray.arr.push({ toJSON: () => viaArray }); 180 + 181 + assertEquals(identify(viaSet), identify(viaSet)); 182 + assertEquals(identify(viaMap), identify(viaMap)); 183 + assertEquals(identify(viaArray), identify(viaArray)); 184 + }); 185 + 186 + Deno.test('toJSON :: array and mutual results still resolve cycles', () => { 187 + const arr: any = {}; 188 + arr.toJSON = () => [arr]; 189 + assertEquals(identify(arr), identify(arr)); 190 + 191 + const a: any = {}; 192 + const b: any = {}; 193 + a.toJSON = () => ({ b }); 194 + b.toJSON = () => ({ a }); 195 + assertEquals(identify(a), identify(a)); 196 + assertEquals(identify(b), identify(b)); 197 + }); 198 + 136 199 // ~> Dates & RegExps 137 200 138 201 Deno.test('dates :: equal instants match, different instants differ', () => {
+11 -10
lib/mod.ts
··· 1 1 function walk(input: any, seen: any[]): string | undefined { 2 2 if (input === null) return 'L'; 3 3 4 - let out: string, i = 0, keys: any, tmp: any; 4 + let out: string, i = 0, keys: any = input, tmp: any = typeof input; 5 5 6 - if ((tmp = typeof input) !== 'object') { 6 + if (tmp !== 'object') { 7 7 if (tmp === 'number') return input - input === 0 ? 'n' + input : 'L'; 8 8 if (tmp === 'string') return 's' + input; 9 9 if (tmp === 'bigint') return 'n' + input; ··· 15 15 if (!is_arr) { 16 16 if (input instanceof Date) return 'd' + +input; 17 17 if (input instanceof RegExp) return 'r' + input.source + input.flags; 18 - 19 - if (typeof input.toJSON === 'function' && !ArrayBuffer.isView(input)) { 20 - input = input.toJSON(); 21 - if (input === null) return 'L'; 22 - if (typeof input !== 'object') return walk(input, seen); 23 - is_arr = Array.isArray(input); 24 - } 25 18 } 26 19 27 20 tmp = seen.indexOf(input); 28 21 if (~tmp) return '~' + (tmp + 1); 29 - seen.push(input); 22 + 23 + if (typeof input.toJSON === 'function' && !ArrayBuffer.isView(input)) { 24 + input = input.toJSON(); 25 + if (input === null || typeof input !== 'object') return walk(input, seen); 26 + tmp = seen.indexOf(input); 27 + if (~tmp) return '~' + (tmp + 1); 28 + is_arr = Array.isArray(input); 29 + } 30 + seen.push(keys); 30 31 31 32 if (is_arr) { 32 33 for (
+1 -1
readme.md
··· 28 28 - 🧬 **Canonical.** The same shape always produces the same id. 29 29 - 🌀 **Deep and cycle-safe.** Handles nested objects, arrays, sets, maps, and circular references. 30 30 - 🏎 **Fast.** See the [benchmarks](#-benchmark). 31 - - 🪶 **Tiny.** Around 535B minified and gzipped, with zero 31 + - 🪶 **Tiny.** Around 543B minified and gzipped, with zero 32 32 [dependencies](https://npm.anvaka.com/#/view/2d/object-identity/). 33 33 34 34 ## ⚙️ Install