Skip to content

Commit cbcf0ae

Browse files
fix: resolve circular references through custom revivers
1 parent 796ea83 commit cbcf0ae

3 files changed

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

81+
if (i in hydrated) {
82+
return (hydrated[index] = reviver(hydrated[i]));
83+
}
84+
8185
hydrating ??= new Set();
8286

8387
if (hydrating.has(i)) {

test/index.test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,3 +1682,81 @@ asyncErrorTests('throws for promise resolving to function', async () => {
16821682
});
16831683

16841684
asyncErrorTests.run();
1685+
1686+
const circularCustomTypes = uvu.suite('circular references through custom types');
1687+
1688+
circularCustomTypes('resolves circular reference through two custom types', () => {
1689+
const foo = new Foo({ name: 'outer' });
1690+
const bar = new Bar({ name: 'inner', ref: foo });
1691+
foo.value.ref = bar;
1692+
1693+
const reducers = {
1694+
Foo: (x) => x instanceof Foo && x.value,
1695+
Bar: (x) => x instanceof Bar && x.value
1696+
};
1697+
const fooCache = new WeakMap();
1698+
const barCache = new WeakMap();
1699+
const revivers = {
1700+
Foo: (x) => {
1701+
let inst = fooCache.get(x);
1702+
if (!inst) {
1703+
inst = Object.create(Foo.prototype);
1704+
fooCache.set(x, inst);
1705+
}
1706+
inst.value = x;
1707+
return inst;
1708+
},
1709+
Bar: (x) => {
1710+
let inst = barCache.get(x);
1711+
if (!inst) {
1712+
inst = Object.create(Bar.prototype);
1713+
barCache.set(x, inst);
1714+
}
1715+
inst.value = x;
1716+
return inst;
1717+
}
1718+
};
1719+
1720+
const json = stringify(foo, reducers);
1721+
const result = parse(json, revivers);
1722+
1723+
assert.ok(result instanceof Foo);
1724+
assert.ok(result.value.ref instanceof Bar);
1725+
assert.is(result.value.ref.value.ref, result);
1726+
});
1727+
1728+
circularCustomTypes('resolves self-referencing custom type', () => {
1729+
const foo = new Foo({ name: 'self' });
1730+
foo.value.ref = foo;
1731+
1732+
const reducers = {
1733+
Foo: (x) => x instanceof Foo && x.value
1734+
};
1735+
const fooCache = new WeakMap();
1736+
const revivers = {
1737+
Foo: (x) => {
1738+
let inst = fooCache.get(x);
1739+
if (!inst) {
1740+
inst = Object.create(Foo.prototype);
1741+
fooCache.set(x, inst);
1742+
}
1743+
inst.value = x;
1744+
return inst;
1745+
}
1746+
};
1747+
1748+
const json = stringify(foo, reducers);
1749+
const result = parse(json, revivers);
1750+
1751+
assert.ok(result instanceof Foo);
1752+
assert.is(result.value.ref, result);
1753+
});
1754+
1755+
circularCustomTypes('still rejects genuinely invalid circular reference', () => {
1756+
assert.throws(
1757+
() => parse('[["Custom", 0]]', { Custom: (v) => v }),
1758+
(error) => error.message === 'Invalid circular reference'
1759+
);
1760+
});
1761+
1762+
circularCustomTypes.run();

0 commit comments

Comments
 (0)