Skip to content

Commit c9098bc

Browse files
fix: use state instead of source in reactive classes (#16239)
* fix: use `state` instead of `source` in reactive classes * fix: use `active_reaction` as indication to use `source` or `state` * fix: cleanup `#initial_reaction` on `teardown` to free memory * fix: use `#source` in `set` too * unused * chore: use WeakRef * use update_version instead of WeakRef in SvelteSet/SvelteMap (#16324) * tidy up * tweak comment to remove active_reaction reference --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 1404623 commit c9098bc

File tree

13 files changed

+318
-49
lines changed

13 files changed

+318
-49
lines changed

packages/svelte/src/internal/client/runtime.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ let write_version = 1;
134134
/** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */
135135
let read_version = 0;
136136

137+
export let update_version = read_version;
138+
137139
// If we are working with a get() chain that has no active container,
138140
// to prevent memory leaks, we skip adding the reaction.
139141
export let skip_reaction = false;
@@ -267,6 +269,7 @@ export function update_reaction(reaction) {
267269
var previous_reaction_sources = source_ownership;
268270
var previous_component_context = component_context;
269271
var previous_untracking = untracking;
272+
var previous_update_version = update_version;
270273

271274
var flags = reaction.f;
272275

@@ -280,7 +283,7 @@ export function update_reaction(reaction) {
280283
source_ownership = null;
281284
set_component_context(reaction.ctx);
282285
untracking = false;
283-
read_version++;
286+
update_version = ++read_version;
284287

285288
reaction.f |= EFFECT_IS_UPDATING;
286289

@@ -368,6 +371,7 @@ export function update_reaction(reaction) {
368371
source_ownership = previous_reaction_sources;
369372
set_component_context(previous_component_context);
370373
untracking = previous_untracking;
374+
update_version = previous_update_version;
371375

372376
reaction.f ^= EFFECT_IS_UPDATING;
373377
}

packages/svelte/src/motion/spring.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { writable } from '../store/shared/index.js';
55
import { loop } from '../internal/client/loop.js';
66
import { raf } from '../internal/client/timing.js';
77
import { is_date } from './utils.js';
8-
import { set, source } from '../internal/client/reactivity/sources.js';
8+
import { set, state } from '../internal/client/reactivity/sources.js';
99
import { render_effect } from '../internal/client/reactivity/effects.js';
1010
import { tag } from '../internal/client/dev/tracing.js';
1111
import { get } from '../internal/client/runtime.js';
@@ -170,9 +170,9 @@ export function spring(value, opts = {}) {
170170
* @since 5.8.0
171171
*/
172172
export class Spring {
173-
#stiffness = source(0.15);
174-
#damping = source(0.8);
175-
#precision = source(0.01);
173+
#stiffness = state(0.15);
174+
#damping = state(0.8);
175+
#precision = state(0.01);
176176

177177
#current;
178178
#target;
@@ -194,8 +194,8 @@ export class Spring {
194194
* @param {SpringOpts} [options]
195195
*/
196196
constructor(value, options = {}) {
197-
this.#current = DEV ? tag(source(value), 'Spring.current') : source(value);
198-
this.#target = DEV ? tag(source(value), 'Spring.target') : source(value);
197+
this.#current = DEV ? tag(state(value), 'Spring.current') : state(value);
198+
this.#target = DEV ? tag(state(value), 'Spring.target') : state(value);
199199

200200
if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1);
201201
if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);

packages/svelte/src/motion/tweened.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { raf } from '../internal/client/timing.js';
66
import { loop } from '../internal/client/loop.js';
77
import { linear } from '../easing/index.js';
88
import { is_date } from './utils.js';
9-
import { set, source } from '../internal/client/reactivity/sources.js';
9+
import { set, state } from '../internal/client/reactivity/sources.js';
1010
import { tag } from '../internal/client/dev/tracing.js';
1111
import { get, render_effect } from 'svelte/internal/client';
1212
import { DEV } from 'esm-env';
@@ -191,8 +191,8 @@ export class Tween {
191191
* @param {TweenedOptions<T>} options
192192
*/
193193
constructor(value, options = {}) {
194-
this.#current = source(value);
195-
this.#target = source(value);
194+
this.#current = state(value);
195+
this.#target = state(value);
196196
this.#defaults = options;
197197

198198
if (DEV) {

packages/svelte/src/reactivity/map.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { DEV } from 'esm-env';
33
import { set, source, state } from '../internal/client/reactivity/sources.js';
44
import { label, tag } from '../internal/client/dev/tracing.js';
5-
import { get } from '../internal/client/runtime.js';
5+
import { get, update_version } from '../internal/client/runtime.js';
66
import { increment } from './utils.js';
77

88
/**
@@ -56,6 +56,7 @@ export class SvelteMap extends Map {
5656
#sources = new Map();
5757
#version = state(0);
5858
#size = state(0);
59+
#update_version = update_version || -1;
5960

6061
/**
6162
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
@@ -79,6 +80,19 @@ export class SvelteMap extends Map {
7980
}
8081
}
8182

83+
/**
84+
* If the source is being created inside the same reaction as the SvelteMap instance,
85+
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
86+
* use `source` so it will be.
87+
*
88+
* @template T
89+
* @param {T} value
90+
* @returns {Source<T>}
91+
*/
92+
#source(value) {
93+
return update_version === this.#update_version ? state(value) : source(value);
94+
}
95+
8296
/** @param {K} key */
8397
has(key) {
8498
var sources = this.#sources;
@@ -87,7 +101,7 @@ export class SvelteMap extends Map {
87101
if (s === undefined) {
88102
var ret = super.get(key);
89103
if (ret !== undefined) {
90-
s = source(0);
104+
s = this.#source(0);
91105

92106
if (DEV) {
93107
tag(s, `SvelteMap get(${label(key)})`);
@@ -123,7 +137,7 @@ export class SvelteMap extends Map {
123137
if (s === undefined) {
124138
var ret = super.get(key);
125139
if (ret !== undefined) {
126-
s = source(0);
140+
s = this.#source(0);
127141

128142
if (DEV) {
129143
tag(s, `SvelteMap get(${label(key)})`);
@@ -154,7 +168,7 @@ export class SvelteMap extends Map {
154168
var version = this.#version;
155169

156170
if (s === undefined) {
157-
s = source(0);
171+
s = this.#source(0);
158172

159173
if (DEV) {
160174
tag(s, `SvelteMap get(${label(key)})`);
@@ -219,8 +233,7 @@ export class SvelteMap extends Map {
219233
if (this.#size.v !== sources.size) {
220234
for (var key of super.keys()) {
221235
if (!sources.has(key)) {
222-
var s = source(0);
223-
236+
var s = this.#source(0);
224237
if (DEV) {
225238
tag(s, `SvelteMap get(${label(key)})`);
226239
}

packages/svelte/src/reactivity/set.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { DEV } from 'esm-env';
33
import { source, set, state } from '../internal/client/reactivity/sources.js';
44
import { label, tag } from '../internal/client/dev/tracing.js';
5-
import { get } from '../internal/client/runtime.js';
5+
import { get, update_version } from '../internal/client/runtime.js';
66
import { increment } from './utils.js';
77

88
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
@@ -50,6 +50,7 @@ export class SvelteSet extends Set {
5050
#sources = new Map();
5151
#version = state(0);
5252
#size = state(0);
53+
#update_version = update_version || -1;
5354

5455
/**
5556
* @param {Iterable<T> | null | undefined} [value]
@@ -75,6 +76,19 @@ export class SvelteSet extends Set {
7576
if (!inited) this.#init();
7677
}
7778

79+
/**
80+
* If the source is being created inside the same reaction as the SvelteSet instance,
81+
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
82+
* use `source` so it will be.
83+
*
84+
* @template T
85+
* @param {T} value
86+
* @returns {Source<T>}
87+
*/
88+
#source(value) {
89+
return update_version === this.#update_version ? state(value) : source(value);
90+
}
91+
7892
// We init as part of the first instance so that we can treeshake this class
7993
#init() {
8094
inited = true;
@@ -116,7 +130,7 @@ export class SvelteSet extends Set {
116130
return false;
117131
}
118132

119-
s = source(true);
133+
s = this.#source(true);
120134

121135
if (DEV) {
122136
tag(s, `SvelteSet has(${label(value)})`);

packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export default test({
88
},
99

1010
test({ assert, target }) {
11-
const [button1, button2] = target.querySelectorAll('button');
11+
const [button1, button2, button3, button4, button5, button6, button7, button8] =
12+
target.querySelectorAll('button');
1213

1314
assert.throws(() => {
1415
button1?.click();
@@ -19,5 +20,35 @@ export default test({
1920
button2?.click();
2021
flushSync();
2122
});
23+
24+
assert.throws(() => {
25+
button3?.click();
26+
flushSync();
27+
}, /state_unsafe_mutation/);
28+
29+
assert.doesNotThrow(() => {
30+
button4?.click();
31+
flushSync();
32+
});
33+
34+
assert.throws(() => {
35+
button5?.click();
36+
flushSync();
37+
}, /state_unsafe_mutation/);
38+
39+
assert.doesNotThrow(() => {
40+
button6?.click();
41+
flushSync();
42+
});
43+
44+
assert.throws(() => {
45+
button7?.click();
46+
flushSync();
47+
}, /state_unsafe_mutation/);
48+
49+
assert.doesNotThrow(() => {
50+
button8?.click();
51+
flushSync();
52+
});
2253
}
2354
});
Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,101 @@
11
<script>
22
import { SvelteMap } from 'svelte/reactivity';
33
4-
let visibleExternal = $state(false);
5-
let external = new SvelteMap();
6-
const throws = $derived.by(() => {
7-
external.set(1, 1);
8-
return external;
4+
let outside_basic = $state(false);
5+
let outside_basic_map = new SvelteMap();
6+
const throw_basic = $derived.by(() => {
7+
outside_basic_map.set(1, 1);
8+
return outside_basic_map;
99
});
1010
11-
let visibleInternal = $state(false);
12-
const works = $derived.by(() => {
13-
let internal = new SvelteMap();
14-
internal.set(1, 1);
15-
return internal;
11+
let inside_basic = $state(false);
12+
const works_basic = $derived.by(() => {
13+
let inside = new SvelteMap();
14+
inside.set(1, 1);
15+
return inside;
16+
});
17+
18+
let outside_has = $state(false);
19+
let outside_has_map = new SvelteMap([[1, 1]]);
20+
const throw_has = $derived.by(() => {
21+
outside_has_map.has(1);
22+
outside_has_map.set(1, 2);
23+
return outside_has_map;
24+
});
25+
26+
let inside_has = $state(false);
27+
const works_has = $derived.by(() => {
28+
let inside = new SvelteMap([[1, 1]]);
29+
inside.has(1);
30+
inside.set(1, 1);
31+
return inside;
32+
});
33+
34+
let outside_get = $state(false);
35+
let outside_get_map = new SvelteMap([[1, 1]]);
36+
const throw_get = $derived.by(() => {
37+
outside_get_map.get(1);
38+
outside_get_map.set(1, 2);
39+
return outside_get_map;
40+
});
41+
42+
let inside_get = $state(false);
43+
const works_get = $derived.by(() => {
44+
let inside = new SvelteMap([[1, 1]]);
45+
inside.get(1);
46+
inside.set(1, 1);
47+
return inside;
48+
});
49+
50+
let outside_values = $state(false);
51+
let outside_values_map = new SvelteMap([[1, 1]]);
52+
const throw_values = $derived.by(() => {
53+
outside_values_map.values(1);
54+
outside_values_map.set(1, 2);
55+
return outside_values_map;
56+
});
57+
58+
let inside_values = $state(false);
59+
const works_values = $derived.by(() => {
60+
let inside = new SvelteMap([[1, 1]]);
61+
inside.values();
62+
inside.set(1, 1);
63+
return inside;
1664
});
1765
</script>
1866

19-
<button onclick={() => (visibleExternal = true)}>external</button>
20-
{#if visibleExternal}
21-
{throws}
67+
<button onclick={() => (outside_basic = true)}>external</button>
68+
{#if outside_basic}
69+
{throw_basic}
70+
{/if}
71+
<button onclick={() => (inside_basic = true)}>internal</button>
72+
{#if inside_basic}
73+
{works_basic}
74+
{/if}
75+
76+
<button onclick={() => (outside_has = true)}>external</button>
77+
{#if outside_has}
78+
{throw_has}
2279
{/if}
23-
<button onclick={() => (visibleInternal = true)}>internal</button>
24-
{#if visibleInternal}
25-
{works}
80+
<button onclick={() => (inside_has = true)}>internal</button>
81+
{#if inside_has}
82+
{works_has}
2683
{/if}
2784

85+
<button onclick={() => (outside_get = true)}>external</button>
86+
{#if outside_get}
87+
{throw_get}
88+
{/if}
89+
<button onclick={() => (inside_get = true)}>internal</button>
90+
{#if inside_get}
91+
{works_get}
92+
{/if}
93+
94+
<button onclick={() => (outside_values = true)}>external</button>
95+
{#if outside_values}
96+
{throw_values}
97+
{/if}
98+
<button onclick={() => (inside_values = true)}>internal</button>
99+
{#if inside_values}
100+
{works_values}
101+
{/if}

packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default test({
88
},
99

1010
test({ assert, target }) {
11-
const [button1, button2] = target.querySelectorAll('button');
11+
const [button1, button2, button3, button4] = target.querySelectorAll('button');
1212

1313
assert.throws(() => {
1414
button1?.click();
@@ -19,5 +19,15 @@ export default test({
1919
button2?.click();
2020
flushSync();
2121
});
22+
23+
assert.throws(() => {
24+
button3?.click();
25+
flushSync();
26+
}, /state_unsafe_mutation/);
27+
28+
assert.doesNotThrow(() => {
29+
button4?.click();
30+
flushSync();
31+
});
2232
}
2333
});

0 commit comments

Comments
 (0)