Skip to content

Commit cd6da94

Browse files
fix: resolve circular references through custom revivers (#154)
* fix: resolve circular references through custom revivers * Update src/parse.js * Update test/index.test.js --------- Co-authored-by: Elliott Johnson <hello+github@ell.iott.dev>
1 parent 06129ad commit cd6da94

3 files changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'devalue': patch
3+
---
4+
5+
fix: resolve circular references through custom revivers when payload is already hydrated

‎src/parse.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ export function unflatten(parsed, revivers) {
7878
i = values.push(value[1]) - 1;
7979
}
8080

81+
// If the payload is already hydrated, its recursion has already
82+
// terminated (e.g. a self-referential object cached itself before
83+
// following its own back-reference), so revive it directly. Falling
84+
// through to the `hydrating` guard here would wrongly reject a valid
85+
// cycle. An actually infinite payload (e.g. `[["Custom", 0]]`) is never
86+
// cached, so it still hits the guard below.
87+
if (Object.hasOwn(hydrated, i)) {
88+
return (hydrated[index] = reviver(hydrated[i]));
89+
}
90+
8191
hydrating ??= new Set();
8292

8393
if (hydrating.has(i)) {

‎test/index.test.js‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,3 +1805,74 @@ asyncErrorTests('throws for promise resolving to function', async () => {
18051805
});
18061806

18071807
asyncErrorTests.run();
1808+
1809+
const circularCustomTypes = uvu.suite('circular references through custom types');
1810+
1811+
circularCustomTypes('resolves circular reference through two custom types', () => {
1812+
const foo = new Foo({ name: 'outer' });
1813+
const bar = new Bar({ name: 'inner', ref: foo });
1814+
foo.value.ref = bar;
1815+
1816+
const reducers = {
1817+
Foo: (x) => x instanceof Foo && x.value,
1818+
Bar: (x) => x instanceof Bar && x.value
1819+
};
1820+
const fooCache = new WeakMap();
1821+
const barCache = new WeakMap();
1822+
const revivers = {
1823+
Foo: (x) => {
1824+
let inst = fooCache.get(x);
1825+
if (!inst) {
1826+
inst = Object.create(Foo.prototype);
1827+
fooCache.set(x, inst);
1828+
}
1829+
inst.value = x;
1830+
return inst;
1831+
},
1832+
Bar: (x) => {
1833+
let inst = barCache.get(x);
1834+
if (!inst) {
1835+
inst = Object.create(Bar.prototype);
1836+
barCache.set(x, inst);
1837+
}
1838+
inst.value = x;
1839+
return inst;
1840+
}
1841+
};
1842+
1843+
const json = stringify(foo, reducers);
1844+
const result = parse(json, revivers);
1845+
1846+
assert.ok(result instanceof Foo);
1847+
assert.ok(result.value.ref instanceof Bar);
1848+
assert.is(result.value.ref.value.ref, result);
1849+
});
1850+
1851+
circularCustomTypes('resolves self-referencing custom type', () => {
1852+
const foo = new Foo({ name: 'self' });
1853+
foo.value.ref = foo;
1854+
1855+
const reducers = {
1856+
Foo: (x) => x instanceof Foo && x.value
1857+
};
1858+
const fooCache = new WeakMap();
1859+
const revivers = {
1860+
Foo: (x) => {
1861+
let inst = fooCache.get(x);
1862+
if (!inst) {
1863+
inst = Object.create(Foo.prototype);
1864+
fooCache.set(x, inst);
1865+
}
1866+
inst.value = x;
1867+
return inst;
1868+
}
1869+
};
1870+
1871+
const json = stringify(foo, reducers);
1872+
const result = parse(json, revivers);
1873+
1874+
assert.ok(result instanceof Foo);
1875+
assert.is(result.value.ref, result);
1876+
});
1877+
1878+
circularCustomTypes.run();

0 commit comments

Comments
 (0)