From de8053ef1da65909959c191f8d19d97497439a21 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 25 Jun 2025 18:13:39 +0200 Subject: [PATCH 1/9] fix: use `state` instead of `source` in reactive classes --- packages/svelte/src/motion/spring.js | 12 +++++------ packages/svelte/src/motion/tweened.js | 6 +++--- packages/svelte/src/reactivity/map.js | 29 ++++++++++++++++++++++++--- packages/svelte/src/reactivity/set.js | 7 ++++++- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 0f3bc6fb9f87..44be1a501b91 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -5,7 +5,7 @@ import { writable } from '../store/shared/index.js'; import { loop } from '../internal/client/loop.js'; import { raf } from '../internal/client/timing.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } from '../internal/client/reactivity/sources.js'; import { render_effect } from '../internal/client/reactivity/effects.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; @@ -170,9 +170,9 @@ export function spring(value, opts = {}) { * @since 5.8.0 */ export class Spring { - #stiffness = source(0.15); - #damping = source(0.8); - #precision = source(0.01); + #stiffness = state(0.15); + #damping = state(0.8); + #precision = state(0.01); #current; #target; @@ -194,8 +194,8 @@ export class Spring { * @param {SpringOpts} [options] */ constructor(value, options = {}) { - this.#current = DEV ? tag(source(value), 'Spring.current') : source(value); - this.#target = DEV ? tag(source(value), 'Spring.target') : source(value); + this.#current = DEV ? tag(state(value), 'Spring.current') : state(value); + this.#target = DEV ? tag(state(value), 'Spring.target') : state(value); if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1); if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1); diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 09bd06c325d5..437c22ec3b2b 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -6,7 +6,7 @@ import { raf } from '../internal/client/timing.js'; import { loop } from '../internal/client/loop.js'; import { linear } from '../easing/index.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get, render_effect } from 'svelte/internal/client'; import { DEV } from 'esm-env'; @@ -191,8 +191,8 @@ export class Tween { * @param {TweenedOptions} options */ constructor(value, options = {}) { - this.#current = source(value); - this.#target = source(value); + this.#current = state(value); + this.#target = state(value); this.#defaults = options; if (DEV) { diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index cd2fac163fc6..bb52cdafc21f 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; +import { get, push_reaction_value } from '../internal/client/runtime.js'; import { increment } from './utils.js'; /** @@ -83,11 +83,13 @@ export class SvelteMap extends Map { has(key) { var sources = this.#sources; var s = sources.get(key); + var is_new = false; if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { s = source(0); + is_new = true; if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -103,6 +105,12 @@ export class SvelteMap extends Map { } get(s); + if (is_new) { + // if it's a new source we want to push to the reactions values + // AFTER we read it so that the effect depends on it but we don't + // trigger an unsafe mutation (since it was created within the derived) + push_reaction_value(s); + } return true; } @@ -119,11 +127,13 @@ export class SvelteMap extends Map { get(key) { var sources = this.#sources; var s = sources.get(key); + var is_new = false; if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { s = source(0); + is_new = true; if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -139,6 +149,12 @@ export class SvelteMap extends Map { } get(s); + if (is_new) { + // if it's a new source we want to push to the reactions values + // AFTER we read it so that the effect depends on it but we don't + // trigger an unsafe mutation (since it was created within the derived) + push_reaction_value(s); + } return super.get(key); } @@ -154,7 +170,7 @@ export class SvelteMap extends Map { var version = this.#version; if (s === undefined) { - s = source(0); + s = state(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -216,11 +232,12 @@ export class SvelteMap extends Map { get(this.#version); var sources = this.#sources; + var new_sources = new WeakSet(); if (this.#size.v !== sources.size) { for (var key of super.keys()) { if (!sources.has(key)) { var s = source(0); - + new_sources.add(s); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); } @@ -232,6 +249,12 @@ export class SvelteMap extends Map { for ([, s] of this.#sources) { get(s); + if (new_sources.has(s)) { + // if it's a new source we want to push to the reactions values + // AFTER we read it so that the effect depends on it but we don't + // trigger an unsafe mutation (since it was created within the derived) + push_reaction_value(s); + } } } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 8a656c2bc14a..7bb5da87052e 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; +import { get, push_reaction_value } from '../internal/client/runtime.js'; import { increment } from './utils.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; @@ -107,6 +107,7 @@ export class SvelteSet extends Set { var has = super.has(value); var sources = this.#sources; var s = sources.get(value); + var is_new = false; if (s === undefined) { if (!has) { @@ -117,6 +118,7 @@ export class SvelteSet extends Set { } s = source(true); + is_new = true; if (DEV) { tag(s, `SvelteSet has(${label(value)})`); @@ -126,6 +128,9 @@ export class SvelteSet extends Set { } get(s); + if (is_new) { + push_reaction_value(s); + } return has; } From 2e69ed4f273af13c6c06e59281d3a086a0c6561f Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 25 Jun 2025 23:19:01 +0200 Subject: [PATCH 2/9] fix: use `active_reaction` as indication to use `source` or `state` --- packages/svelte/src/reactivity/map.js | 54 +++++---- packages/svelte/src/reactivity/set.js | 32 ++++-- .../side-effect-derived-map/_config.js | 33 +++++- .../side-effect-derived-map/main.svelte | 106 +++++++++++++++--- .../side-effect-derived-set/_config.js | 12 +- .../side-effect-derived-set/main.svelte | 51 ++++++--- .../side-effect-derived-spring/_config.js | 23 ++++ .../side-effect-derived-spring/main.svelte | 26 +++++ .../side-effect-derived-tween/_config.js | 23 ++++ .../side-effect-derived-tween/main.svelte | 26 +++++ 10 files changed, 318 insertions(+), 68 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index bb52cdafc21f..e4a54d1792e5 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -1,8 +1,8 @@ -/** @import { Source } from '#client' */ +/** @import { Reaction, Source } from '#client' */ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get, push_reaction_value } from '../internal/client/runtime.js'; +import { active_reaction, get, push_reaction_value } from '../internal/client/runtime.js'; import { increment } from './utils.js'; /** @@ -56,6 +56,8 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); + /**@type {Reaction | null} */ + #initial_reaction = null; /** * @param {Iterable | null | undefined} [value] @@ -63,6 +65,8 @@ export class SvelteMap extends Map { constructor(value) { super(); + this.#initial_reaction = active_reaction; + if (DEV) { // If the value is invalid then the native exception will fire here value = new Map(value); @@ -79,17 +83,31 @@ export class SvelteMap extends Map { } } + /** + * If the active_reaction is the same as the reaction that created this SvelteMap, + * we use state so that it will not be a dependency of the reaction. Otherwise we + * use source so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + if (this.#initial_reaction === active_reaction) { + return state(value); + } + return source(value); + } + /** @param {K} key */ has(key) { var sources = this.#sources; var s = sources.get(key); - var is_new = false; if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); - is_new = true; + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -105,12 +123,6 @@ export class SvelteMap extends Map { } get(s); - if (is_new) { - // if it's a new source we want to push to the reactions values - // AFTER we read it so that the effect depends on it but we don't - // trigger an unsafe mutation (since it was created within the derived) - push_reaction_value(s); - } return true; } @@ -127,13 +139,11 @@ export class SvelteMap extends Map { get(key) { var sources = this.#sources; var s = sources.get(key); - var is_new = false; if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); - is_new = true; + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -149,12 +159,6 @@ export class SvelteMap extends Map { } get(s); - if (is_new) { - // if it's a new source we want to push to the reactions values - // AFTER we read it so that the effect depends on it but we don't - // trigger an unsafe mutation (since it was created within the derived) - push_reaction_value(s); - } return super.get(key); } @@ -232,12 +236,10 @@ export class SvelteMap extends Map { get(this.#version); var sources = this.#sources; - var new_sources = new WeakSet(); if (this.#size.v !== sources.size) { for (var key of super.keys()) { if (!sources.has(key)) { - var s = source(0); - new_sources.add(s); + var s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); } @@ -249,12 +251,6 @@ export class SvelteMap extends Map { for ([, s] of this.#sources) { get(s); - if (new_sources.has(s)) { - // if it's a new source we want to push to the reactions values - // AFTER we read it so that the effect depends on it but we don't - // trigger an unsafe mutation (since it was created within the derived) - push_reaction_value(s); - } } } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 7bb5da87052e..3f6c0db8e895 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,8 +1,8 @@ -/** @import { Source } from '#client' */ +/** @import { Reaction, Source } from '#client' */ import { DEV } from 'esm-env'; import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get, push_reaction_value } from '../internal/client/runtime.js'; +import { active_reaction, get } from '../internal/client/runtime.js'; import { increment } from './utils.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; @@ -51,12 +51,17 @@ export class SvelteSet extends Set { #version = state(0); #size = state(0); + /**@type {Reaction | null}*/ + #initial_reaction = null; + /** * @param {Iterable | null | undefined} [value] */ constructor(value) { super(); + this.#initial_reaction = active_reaction; + if (DEV) { // If the value is invalid then the native exception will fire here value = new Set(value); @@ -75,6 +80,22 @@ export class SvelteSet extends Set { if (!inited) this.#init(); } + /** + * If the active_reaction is the same as the reaction that created this SvelteMap, + * we use state so that it will not be a dependency of the reaction. Otherwise we + * use source so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + if (this.#initial_reaction === active_reaction) { + return state(value); + } + return source(value); + } + // We init as part of the first instance so that we can treeshake this class #init() { inited = true; @@ -107,7 +128,6 @@ export class SvelteSet extends Set { var has = super.has(value); var sources = this.#sources; var s = sources.get(value); - var is_new = false; if (s === undefined) { if (!has) { @@ -117,8 +137,7 @@ export class SvelteSet extends Set { return false; } - s = source(true); - is_new = true; + s = this.#source(true); if (DEV) { tag(s, `SvelteSet has(${label(value)})`); @@ -128,9 +147,6 @@ export class SvelteSet extends Set { } get(s); - if (is_new) { - push_reaction_value(s); - } return has; } diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js index 5ad6f57e311f..c10dc7fb555f 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js @@ -8,7 +8,8 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4, button5, button6, button7, button8] = + target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +20,35 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); + + assert.throws(() => { + button5?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button6?.click(); + flushSync(); + }); + + assert.throws(() => { + button7?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button8?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte index bdd5ccb75c91..c37f37ceb6b7 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte @@ -1,27 +1,101 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throw_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has} + {throw_has} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has} + {works_has} {/if} + +{#if outside_get} + {throw_get} +{/if} + +{#if inside_get} + {works_get} +{/if} + + +{#if outside_values} + {throw_values} +{/if} + +{#if inside_values} + {works_values} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js index 5ad6f57e311f..5cf066fb8a0c 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js @@ -8,7 +8,7 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4] = target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +19,15 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte index 8564f6e7c48e..1d6735ba64b4 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte @@ -1,27 +1,52 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has_delete} + {throws_has_delete} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has_delete} + {works_has_delete} {/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js new file mode 100644 index 000000000000..5ad6f57e311f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte new file mode 100644 index 000000000000..b0818deca960 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js new file mode 100644 index 000000000000..5ad6f57e311f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte new file mode 100644 index 000000000000..bd007f2b500d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file From fb627c097b21f08c5f77b4434f3d3bc02f4434b7 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Thu, 26 Jun 2025 18:13:36 +0200 Subject: [PATCH 3/9] fix: cleanup `#initial_reaction` on `teardown` to free memory --- packages/svelte/src/reactivity/map.js | 10 +++++++++- packages/svelte/src/reactivity/set.js | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index e4a54d1792e5..11162775c73b 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -4,6 +4,7 @@ import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; import { active_reaction, get, push_reaction_value } from '../internal/client/runtime.js'; import { increment } from './utils.js'; +import { teardown } from '../internal/client/reactivity/effects.js'; /** * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object. @@ -65,7 +66,14 @@ export class SvelteMap extends Map { constructor(value) { super(); - this.#initial_reaction = active_reaction; + if (active_reaction !== null) { + this.#initial_reaction = active_reaction; + // since we only need `initial_reaction` as long as we are in a derived/effect we can + // safely create a teardown function that will reset it to null and allow for GC + teardown(() => { + this.#initial_reaction = null; + }); + } if (DEV) { // If the value is invalid then the native exception will fire here diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 3f6c0db8e895..e7c429c22f69 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -4,6 +4,7 @@ import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; import { active_reaction, get } from '../internal/client/runtime.js'; import { increment } from './utils.js'; +import { teardown } from '../internal/client/reactivity/effects.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; @@ -60,7 +61,14 @@ export class SvelteSet extends Set { constructor(value) { super(); - this.#initial_reaction = active_reaction; + if (active_reaction !== null) { + this.#initial_reaction = active_reaction; + // since we only need `initial_reaction` as long as we are in a derived/effect we can + // safely create a teardown function that will reset it to null and allow for GC + teardown(() => { + this.#initial_reaction = null; + }); + } if (DEV) { // If the value is invalid then the native exception will fire here From 5670a210af72549e08a9d08efd0af403ca097fa6 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Mon, 7 Jul 2025 11:09:24 +0200 Subject: [PATCH 4/9] fix: use `#source` in `set` too --- packages/svelte/src/reactivity/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 11162775c73b..150aae1dc8b3 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -182,7 +182,7 @@ export class SvelteMap extends Map { var version = this.#version; if (s === undefined) { - s = state(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); From f261686140db680de87fbede79205accd43dabea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 13:03:14 -0400 Subject: [PATCH 5/9] unused --- packages/svelte/src/reactivity/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 150aae1dc8b3..64eb2a627629 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_reaction, get, push_reaction_value } from '../internal/client/runtime.js'; +import { active_reaction, get } from '../internal/client/runtime.js'; import { increment } from './utils.js'; import { teardown } from '../internal/client/reactivity/effects.js'; From e3072c9906ecd8f5e8280ed32a193fdb2f0240d4 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Mon, 7 Jul 2025 22:35:08 +0200 Subject: [PATCH 6/9] chore: use WeakRef --- packages/svelte/src/reactivity/map.js | 14 ++++++-------- packages/svelte/src/reactivity/set.js | 14 ++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 64eb2a627629..35946c5b088e 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -57,7 +57,7 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); - /**@type {Reaction | null} */ + /**@type {WeakRef | null} */ #initial_reaction = null; /** @@ -67,12 +67,10 @@ export class SvelteMap extends Map { super(); if (active_reaction !== null) { - this.#initial_reaction = active_reaction; - // since we only need `initial_reaction` as long as we are in a derived/effect we can - // safely create a teardown function that will reset it to null and allow for GC - teardown(() => { - this.#initial_reaction = null; - }); + // we use a WeakRef (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) + // so that if this Map is somehow stored outside of the active reaction, + // it will not prevent the reaction from being garbage collected. + this.#initial_reaction = new WeakRef(active_reaction); } if (DEV) { @@ -101,7 +99,7 @@ export class SvelteMap extends Map { * @returns {Source} */ #source(value) { - if (this.#initial_reaction === active_reaction) { + if (this.#initial_reaction !== null && this.#initial_reaction.deref() === active_reaction) { return state(value); } return source(value); diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index e7c429c22f69..66c015114238 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -52,7 +52,7 @@ export class SvelteSet extends Set { #version = state(0); #size = state(0); - /**@type {Reaction | null}*/ + /**@type {WeakRef | null}*/ #initial_reaction = null; /** @@ -62,12 +62,10 @@ export class SvelteSet extends Set { super(); if (active_reaction !== null) { - this.#initial_reaction = active_reaction; - // since we only need `initial_reaction` as long as we are in a derived/effect we can - // safely create a teardown function that will reset it to null and allow for GC - teardown(() => { - this.#initial_reaction = null; - }); + // we use a WeakRef (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) + // so that if this Map is somehow stored outside of the active reaction, + // it will not prevent the reaction from being garbage collected. + this.#initial_reaction = new WeakRef(active_reaction); } if (DEV) { @@ -98,7 +96,7 @@ export class SvelteSet extends Set { * @returns {Source} */ #source(value) { - if (this.#initial_reaction === active_reaction) { + if (this.#initial_reaction !== null && this.#initial_reaction.deref() === active_reaction) { return state(value); } return source(value); From 3ab41d6d07f1a8bce831c9ac2f6fb06e9e3d3f95 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Jul 2025 09:09:54 -0400 Subject: [PATCH 7/9] use update_version instead of WeakRef in SvelteSet/SvelteMap (#16324) --- packages/svelte/src/internal/client/runtime.js | 6 +++++- packages/svelte/src/reactivity/map.js | 13 +++++-------- packages/svelte/src/reactivity/set.js | 13 ++++--------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fce6c78b56e4..38ca0dff5c8d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -132,6 +132,8 @@ let write_version = 1; /** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */ let read_version = 0; +export let update_version = read_version; + // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; @@ -265,6 +267,7 @@ export function update_reaction(reaction) { var previous_reaction_sources = source_ownership; var previous_component_context = component_context; var previous_untracking = untracking; + var previous_update_version = update_version; var flags = reaction.f; @@ -278,7 +281,7 @@ export function update_reaction(reaction) { source_ownership = null; set_component_context(reaction.ctx); untracking = false; - read_version++; + update_version = ++read_version; reaction.f |= EFFECT_IS_UPDATING; @@ -366,6 +369,7 @@ export function update_reaction(reaction) { source_ownership = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + update_version = previous_update_version; reaction.f ^= EFFECT_IS_UPDATING; } diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 35946c5b088e..93668fa01a13 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_reaction, get } from '../internal/client/runtime.js'; +import { active_reaction, get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; import { teardown } from '../internal/client/reactivity/effects.js'; @@ -57,8 +57,7 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); - /**@type {WeakRef | null} */ - #initial_reaction = null; + #update_version = -1; /** * @param {Iterable | null | undefined} [value] @@ -67,10 +66,7 @@ export class SvelteMap extends Map { super(); if (active_reaction !== null) { - // we use a WeakRef (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) - // so that if this Map is somehow stored outside of the active reaction, - // it will not prevent the reaction from being garbage collected. - this.#initial_reaction = new WeakRef(active_reaction); + this.#update_version = update_version; } if (DEV) { @@ -99,9 +95,10 @@ export class SvelteMap extends Map { * @returns {Source} */ #source(value) { - if (this.#initial_reaction !== null && this.#initial_reaction.deref() === active_reaction) { + if (update_version === this.#update_version) { return state(value); } + return source(value); } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 66c015114238..5a236222c0fd 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_reaction, get } from '../internal/client/runtime.js'; +import { active_reaction, get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; import { teardown } from '../internal/client/reactivity/effects.js'; @@ -51,9 +51,7 @@ export class SvelteSet extends Set { #sources = new Map(); #version = state(0); #size = state(0); - - /**@type {WeakRef | null}*/ - #initial_reaction = null; + #update_version = -1; /** * @param {Iterable | null | undefined} [value] @@ -62,10 +60,7 @@ export class SvelteSet extends Set { super(); if (active_reaction !== null) { - // we use a WeakRef (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) - // so that if this Map is somehow stored outside of the active reaction, - // it will not prevent the reaction from being garbage collected. - this.#initial_reaction = new WeakRef(active_reaction); + this.#update_version = update_version; } if (DEV) { @@ -96,7 +91,7 @@ export class SvelteSet extends Set { * @returns {Source} */ #source(value) { - if (this.#initial_reaction !== null && this.#initial_reaction.deref() === active_reaction) { + if (this.#update_version === update_version) { return state(value); } return source(value); From 69ebe294177af0bf66e3cd12dc752feab6730b77 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Jul 2025 09:21:10 -0400 Subject: [PATCH 8/9] tidy up --- packages/svelte/src/reactivity/map.js | 17 ++++------------- packages/svelte/src/reactivity/set.js | 16 ++++------------ 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 93668fa01a13..fe0b9ef1fa0e 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -1,10 +1,9 @@ -/** @import { Reaction, Source } from '#client' */ +/** @import { Source } from '#client' */ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_reaction, get, update_version } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; -import { teardown } from '../internal/client/reactivity/effects.js'; /** * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object. @@ -57,7 +56,7 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); - #update_version = -1; + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -65,10 +64,6 @@ export class SvelteMap extends Map { constructor(value) { super(); - if (active_reaction !== null) { - this.#update_version = update_version; - } - if (DEV) { // If the value is invalid then the native exception will fire here value = new Map(value); @@ -95,11 +90,7 @@ export class SvelteMap extends Map { * @returns {Source} */ #source(value) { - if (update_version === this.#update_version) { - return state(value); - } - - return source(value); + return update_version === this.#update_version ? state(value) : source(value); } /** @param {K} key */ diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 5a236222c0fd..5ba951d40a6e 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,10 +1,9 @@ -/** @import { Reaction, Source } from '#client' */ +/** @import { Source } from '#client' */ import { DEV } from 'esm-env'; import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_reaction, get, update_version } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; -import { teardown } from '../internal/client/reactivity/effects.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; @@ -51,7 +50,7 @@ export class SvelteSet extends Set { #sources = new Map(); #version = state(0); #size = state(0); - #update_version = -1; + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -59,10 +58,6 @@ export class SvelteSet extends Set { constructor(value) { super(); - if (active_reaction !== null) { - this.#update_version = update_version; - } - if (DEV) { // If the value is invalid then the native exception will fire here value = new Set(value); @@ -91,10 +86,7 @@ export class SvelteSet extends Set { * @returns {Source} */ #source(value) { - if (this.#update_version === update_version) { - return state(value); - } - return source(value); + return update_version === this.#update_version ? state(value) : source(value); } // We init as part of the first instance so that we can treeshake this class From b208f71d3c31ca6e82892d1e7acef08ade06baf0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Jul 2025 09:26:01 -0400 Subject: [PATCH 9/9] tweak comment to remove active_reaction reference --- packages/svelte/src/reactivity/map.js | 6 +++--- packages/svelte/src/reactivity/set.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index fe0b9ef1fa0e..4fa2dfd7b2fc 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -81,9 +81,9 @@ export class SvelteMap extends Map { } /** - * If the active_reaction is the same as the reaction that created this SvelteMap, - * we use state so that it will not be a dependency of the reaction. Otherwise we - * use source so it will be. + * If the source is being created inside the same reaction as the SvelteMap instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. * * @template T * @param {T} value diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 5ba951d40a6e..9e3c330beb36 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -77,9 +77,9 @@ export class SvelteSet extends Set { } /** - * If the active_reaction is the same as the reaction that created this SvelteMap, - * we use state so that it will not be a dependency of the reaction. Otherwise we - * use source so it will be. + * If the source is being created inside the same reaction as the SvelteSet instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. * * @template T * @param {T} value