Skip to content

Commit 7411068

Browse files
trueadmRich-Harris
andauthored
fix: ensure internal cloning can work circular values (#14347)
* fix: ensure internal cloning can work circular values * better fixc * 'original' feels slightly clearer than 'json_instance' * use an optional parameter, so we can omit it in most cases * Update packages/svelte/src/internal/shared/clone.js Co-authored-by: Rich Harris <[email protected]> --------- Co-authored-by: Rich Harris <[email protected]>
1 parent f6117bb commit 7411068

File tree

4 files changed

+64
-2
lines changed

4 files changed

+64
-2
lines changed

.changeset/cool-trains-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure internal cloning can work circular values

packages/svelte/src/internal/shared/clone.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ export function snapshot(value, skip_warning = false) {
4949
* @param {Map<T, Snapshot<T>>} cloned
5050
* @param {string} path
5151
* @param {string[]} paths
52+
* @param {null | T} original The original value, if `value` was produced from a `toJSON` call
5253
* @returns {Snapshot<T>}
5354
*/
54-
function clone(value, cloned, path, paths) {
55+
function clone(value, cloned, path, paths, original = null) {
5556
if (typeof value === 'object' && value !== null) {
5657
const unwrapped = cloned.get(value);
5758
if (unwrapped !== undefined) return unwrapped;
@@ -63,6 +64,10 @@ function clone(value, cloned, path, paths) {
6364
const copy = /** @type {Snapshot<any>} */ ([]);
6465
cloned.set(value, copy);
6566

67+
if (original !== null) {
68+
cloned.set(original, copy);
69+
}
70+
6671
for (let i = 0; i < value.length; i += 1) {
6772
copy.push(clone(value[i], cloned, DEV ? `${path}[${i}]` : path, paths));
6873
}
@@ -75,6 +80,10 @@ function clone(value, cloned, path, paths) {
7580
const copy = {};
7681
cloned.set(value, copy);
7782

83+
if (original !== null) {
84+
cloned.set(original, copy);
85+
}
86+
7887
for (var key in value) {
7988
// @ts-expect-error
8089
copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths);
@@ -92,7 +101,9 @@ function clone(value, cloned, path, paths) {
92101
/** @type {T & { toJSON(): any } } */ (value).toJSON(),
93102
cloned,
94103
DEV ? `${path}.toJSON()` : path,
95-
paths
104+
paths,
105+
// Associate the instance with the toJSON clone
106+
value
96107
);
97108
}
98109
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
compileOptions: {
5+
dev: true
6+
},
7+
8+
async test({ assert, logs }) {
9+
var a = {
10+
a: {}
11+
};
12+
a.a = a;
13+
14+
var b = {
15+
a: {
16+
b: {}
17+
}
18+
};
19+
b.a.b = b;
20+
21+
assert.deepEqual(logs, ['init', a, 'init', b]);
22+
}
23+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
class A {
3+
toJSON(){
4+
return {
5+
a: this
6+
}
7+
}
8+
}
9+
const state = $state(new A());
10+
$inspect(state);
11+
12+
class B {
13+
toJSON(){
14+
return {
15+
a: {
16+
b: this
17+
}
18+
}
19+
}
20+
}
21+
const state2 = $state(new B());
22+
$inspect(state2);
23+
</script>

0 commit comments

Comments
 (0)