From 0b519f1c34bf5a835792418b08a9bbfcdc9cbbd2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 16 Nov 2025 12:38:20 -0500 Subject: [PATCH 01/26] unskip failing test --- .../tests/runtime-runes/samples/async-each-overlap/_config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js index d03f9ad09d..8efd2c7a40 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js @@ -2,7 +2,6 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - skip: true, async test({ assert, target }) { const [add, shift] = target.querySelectorAll('button'); From 37d270b21fb56810d7d433cce0ac2b50531bdf6b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Feb 2026 15:58:38 -0500 Subject: [PATCH 02/26] WIP --- .../src/internal/client/reactivity/batch.js | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index eede5097f7..b75b629f57 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -524,19 +524,46 @@ export class Batch { } apply() { + if (!batches.has(this)) { + console.warn('wtf', this.id); + } + if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return; + console.log(this.id, batches.size); + // if there are multiple batches, we are 'time travelling' — // we need to override values with the ones in this batch... batch_values = new Map(this.current); + var is_earlier = true; + // ...and undo changes belonging to other batches + // that don't coincide with this batch for (const batch of batches) { - if (batch === this) continue; + if (batch === this) { + is_earlier = false; + continue; + } + + var coincides = false; - for (const [source, previous] of batch.previous) { - if (!batch_values.has(source)) { - batch_values.set(source, previous); + if (is_earlier) { + for (const source of batch.current.keys()) { + if (this.current.has(source)) { + coincides = true; + break; + } + } + } + + console.log(batch.id, { coincides }); + + if (!coincides) { + for (const [source, previous] of batch.previous) { + if (!batch_values.has(source)) { + batch_values.set(source, previous); + } } } } From c04f69b5f84e96c6d42c43b1a158240ee8801f60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Feb 2026 19:32:24 -0500 Subject: [PATCH 03/26] fix: preserve each items that are needed by pending batches --- .changeset/spicy-teeth-tan.md | 5 ++ .../src/internal/client/dom/blocks/each.js | 51 ++++++++++++--- .../svelte/src/internal/client/types.d.ts | 3 + .../async-each-preserve-pending/_config.js | 65 +++++++++++++++++++ .../async-each-preserve-pending/main.svelte | 29 +++++++++ 5 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 .changeset/spicy-teeth-tan.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/main.svelte diff --git a/.changeset/spicy-teeth-tan.md b/.changeset/spicy-teeth-tan.md new file mode 100644 index 0000000000..4497392a9f --- /dev/null +++ b/.changeset/spicy-teeth-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve each items that are needed by pending batches diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 7ae02d073c..fe38ed892f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -29,6 +29,7 @@ import { block, branch, destroy_effect, + move_effect, pause_effect, resume_effect } from '../../reactivity/effects.js'; @@ -83,7 +84,7 @@ function pause_effects(state, to_destroy, controlled_anchor) { if (group.pending.size === 0) { var groups = /** @type {Set} */ (state.outrogroups); - destroy_effects(array_from(group.done)); + destroy_effects(state, array_from(group.done)); groups.delete(group); if (groups.size === 0) { @@ -114,7 +115,7 @@ function pause_effects(state, to_destroy, controlled_anchor) { state.items.clear(); } - destroy_effects(to_destroy, !fast_path); + destroy_effects(state, to_destroy, !fast_path); } else { group = { pending: new Set(to_destroy), @@ -126,14 +127,34 @@ function pause_effects(state, to_destroy, controlled_anchor) { } /** + * @param {EachState} state * @param {Effect[]} to_destroy * @param {boolean} remove_dom */ -function destroy_effects(to_destroy, remove_dom = true) { - // TODO only destroy effects if no pending batch needs them. otherwise, - // just re-add the `EFFECT_OFFSCREEN` flag +function destroy_effects(state, to_destroy, remove_dom = true) { + /** @type {Set} */ + var preserved_effects = new Set(); + + // The loop-in-a-loop isn't ideal, but we should only hit this in relatively rare cases + if (state.pending.size > 0) { + for (const keys of state.pending.values()) { + for (const key of keys) { + preserved_effects.add(/** @type {EachItem} */ (state.items.get(key)).e); + } + } + } + for (var i = 0; i < to_destroy.length; i++) { - destroy_effect(to_destroy[i], remove_dom); + var e = to_destroy[i]; + + if (preserved_effects.has(e)) { + e.f |= EFFECT_OFFSCREEN; + + const fragment = document.createDocumentFragment(); + move_effect(e, fragment); + } else { + destroy_effect(to_destroy[i], remove_dom); + } } } @@ -185,9 +206,17 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {V[]} */ var array; + /** @type {Map>} */ + var pending = new Map(); + var first_run = true; - function commit() { + /** + * @param {Batch} batch + */ + function commit(batch) { + state.pending.delete(batch); + state.fallback = fallback; reconcile(state, array, anchor, flags, get_key); @@ -314,6 +343,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } if (!first_run) { + pending.set(batch, keys); + if (defer) { for (const [key, item] of items) { if (!keys.has(key)) { @@ -326,7 +357,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f // TODO presumably we need to do something here? }); } else { - commit(); + commit(batch); } } @@ -345,7 +376,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f }); /** @type {EachState} */ - var state = { effect, flags, items, outrogroups: null, fallback }; + var state = { effect, flags, items, pending, outrogroups: null, fallback }; first_run = false; @@ -544,7 +575,7 @@ function reconcile(state, array, anchor, flags, get_key) { if (state.outrogroups !== null) { for (const group of state.outrogroups) { if (group.pending.size === 0) { - destroy_effects(array_from(group.done)); + destroy_effects(state, array_from(group.done)); state.outrogroups?.delete(group); } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 443c21010e..f4fc81170d 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -1,5 +1,6 @@ import type { Store } from '#shared'; import { STATE_SYMBOL } from './constants.js'; +import type { Batch } from './reactivity/batch.js'; import type { Effect, Source, Value } from './reactivity/types.js'; declare global { @@ -84,6 +85,8 @@ export type EachState = { flags: number; /** a key -> item lookup */ items: Map; + /** a batch -> keys lookup of all keys that are still needed */ + pending: Map>; /** all outro groups that this item is a part of */ outrogroups: Set | null; /** `{:else}` effect */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/_config.js new file mode 100644 index 0000000000..c4efa873bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/_config.js @@ -0,0 +1,65 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [add, shift] = target.querySelectorAll('button'); + + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

1

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

1

+

2

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

1

+

2

+

3

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

1

+

2

+

3

+

4

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/main.svelte new file mode 100644 index 0000000000..55d894d240 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-preserve-pending/main.svelte @@ -0,0 +1,29 @@ + + + + + +{#each values as v} +

{await push(v)}

+{/each} From 6e8c746b58c4a4efda807d82ae88c02d0f0a8a81 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Feb 2026 20:35:50 -0500 Subject: [PATCH 04/26] save some work --- packages/svelte/src/internal/client/dom/blocks/each.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index fe38ed892f..a03f1b316f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -132,11 +132,13 @@ function pause_effects(state, to_destroy, controlled_anchor) { * @param {boolean} remove_dom */ function destroy_effects(state, to_destroy, remove_dom = true) { - /** @type {Set} */ - var preserved_effects = new Set(); + /** @type {Set | undefined} */ + var preserved_effects; // The loop-in-a-loop isn't ideal, but we should only hit this in relatively rare cases if (state.pending.size > 0) { + preserved_effects = new Set(); + for (const keys of state.pending.values()) { for (const key of keys) { preserved_effects.add(/** @type {EachItem} */ (state.items.get(key)).e); @@ -147,7 +149,7 @@ function destroy_effects(state, to_destroy, remove_dom = true) { for (var i = 0; i < to_destroy.length; i++) { var e = to_destroy[i]; - if (preserved_effects.has(e)) { + if (preserved_effects?.has(e)) { e.f |= EFFECT_OFFSCREEN; const fragment = document.createDocumentFragment(); From 484f84151417f715b5cf2bd731433b2e8601a4f4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Feb 2026 20:52:02 -0500 Subject: [PATCH 05/26] fix --- .../svelte/src/internal/client/dom/blocks/each.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index a03f1b316f..cac980aa9b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -241,6 +241,13 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } + /** + * @param {Batch} batch + */ + function discard(batch) { + state.pending.delete(batch); + } + var effect = block(() => { array = /** @type {V[]} */ (get(each_array)); var length = array.length; @@ -355,9 +362,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } batch.oncommit(commit); - batch.ondiscard(() => { - // TODO presumably we need to do something here? - }); + batch.ondiscard(discard); } else { commit(batch); } From a6a548c6f6c3f8d7fcadcc241ff1713938c5a593 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Feb 2026 21:00:46 -0500 Subject: [PATCH 06/26] tidy up --- packages/svelte/src/internal/client/reactivity/batch.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index dd3d7142c3..949e8f7ce1 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -524,14 +524,8 @@ export class Batch { } apply() { - if (!batches.has(this)) { - console.warn('wtf', this.id); - } - if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return; - console.log(this.id, batches.size); - // if there are multiple batches, we are 'time travelling' — // we need to override values with the ones in this batch... batch_values = new Map(this.current); @@ -557,8 +551,6 @@ export class Batch { } } - console.log(batch.id, { coincides }); - if (!coincides) { for (const [source, previous] of batch.previous) { if (!batch_values.has(source)) { From 2cb3759e47d9223e588fa33f01364ef60e910ed6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Feb 2026 21:05:01 -0500 Subject: [PATCH 07/26] fix test --- .../samples/async-each-overlap/_config.js | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js index 8efd2c7a40..670bc9954d 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js @@ -3,6 +3,8 @@ import { test } from '../../test'; export default test({ async test({ assert, target }) { + await tick(); + const [add, shift] = target.querySelectorAll('button'); add.click(); @@ -12,21 +14,19 @@ export default test({ add.click(); await tick(); - // TODO pending count / number of pushes is off - assert.htmlEqual( target.innerHTML, ` - - -

pending=6 values.length=1 values=[1]

-
not keyed: -
1
-
-
keyed: -
1
-
- ` + + +

pending=6 values.length=1 values=[1]

+
not keyed: +
1
+
+
keyed: +
1
+
+ ` ); shift.click(); @@ -36,18 +36,18 @@ export default test({ assert.htmlEqual( target.innerHTML, ` - - -

pending=4 values.length=2 values=[1,2]

-
not keyed: -
1
-
2
-
-
keyed: -
1
-
2
-
- ` + + +

pending=4 values.length=2 values=[1,2]

+
not keyed: +
1
+
2
+
+
keyed: +
1
+
2
+
+ ` ); shift.click(); @@ -57,20 +57,20 @@ export default test({ assert.htmlEqual( target.innerHTML, ` - - -

pending=2 values.length=3 values=[1,2,3]

-
not keyed: -
1
-
2
-
3
-
-
keyed: -
1
-
2
-
3
-
- ` + + +

pending=2 values.length=3 values=[1,2,3]

+
not keyed: +
1
+
2
+
3
+
+
keyed: +
1
+
2
+
3
+
+ ` ); shift.click(); @@ -80,22 +80,22 @@ export default test({ assert.htmlEqual( target.innerHTML, ` - - -

pending=0 values.length=4 values=[1,2,3,4]

-
not keyed: -
1
-
2
-
3
-
4
-
-
keyed: -
1
-
2
-
3
-
4
-
- ` + + +

pending=0 values.length=4 values=[1,2,3,4]

+
not keyed: +
1
+
2
+
3
+
4
+
+
keyed: +
1
+
2
+
3
+
4
+
+ ` ); } }); From 43ed2ba594908b931449ecfc112a15267393924c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 13:42:37 -0400 Subject: [PATCH 08/26] better test --- .../samples/async-each-overlap/_config.js | 106 +++++++++++++++++- .../samples/async-each-overlap/main.svelte | 8 +- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js index 670bc9954d..2dad5babea 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js @@ -5,7 +5,7 @@ export default test({ async test({ assert, target }) { await tick(); - const [add, shift] = target.querySelectorAll('button'); + const [add, shift, pop] = target.querySelectorAll('button'); add.click(); await tick(); @@ -19,6 +19,7 @@ export default test({ ` +

pending=6 values.length=1 values=[1]

not keyed:
1
@@ -38,6 +39,7 @@ export default test({ ` +

pending=4 values.length=2 values=[1,2]

not keyed:
1
@@ -59,6 +61,7 @@ export default test({ ` +

pending=2 values.length=3 values=[1,2,3]

not keyed:
1
@@ -82,6 +85,7 @@ export default test({ ` +

pending=0 values.length=4 values=[1,2,3,4]

not keyed:
1
@@ -97,5 +101,105 @@ export default test({
` ); + + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=8 values.length=4 values=[1,2,3,4]

+
not keyed: +
1
+
2
+
3
+
4
+
+
keyed: +
1
+
2
+
3
+
4
+
+ ` + ); + + // pop should have no effect until earlier promises have also resolved + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=6 values.length=4 values=[1,2,3,4]

+
not keyed: +
1
+
2
+
3
+
4
+
+
keyed: +
1
+
2
+
3
+
4
+
+ ` + ); + + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.length=8 values=[1,2,3,4,5,6,7,8]

+
not keyed: +
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
+
keyed: +
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
+ ` + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte index af9d395457..e41b6926b8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte @@ -11,18 +11,14 @@ return p.promise; } - function shift() { - const fn = queue.shift(); - if (fn) fn(); - } - function addValue() { values.push(values.length+1); } - + +

pending={$effect.pending()} From 903eedb861aad4500dd817470c2df2f84dee6f5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 20:01:38 -0400 Subject: [PATCH 09/26] progress i think? --- .../src/internal/client/reactivity/batch.js | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0d66fae02a..d29eae450c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -166,6 +166,9 @@ export class Batch { #decrement_queued = false; + /** @type {Set} */ + #blockers = new Set(); + #is_deferred() { return this.is_fork || this.#blocking_pending > 0; } @@ -241,7 +244,7 @@ export class Batch { collected_effects = null; legacy_updates = null; - if (this.#is_deferred()) { + if (this.#is_deferred() || this.#blockers.size > 0) { this.#defer_effects(render_effects); this.#defer_effects(effects); @@ -509,7 +512,10 @@ export class Batch { this.#pending -= 1; if (blocking) this.#blocking_pending -= 1; - if (this.#decrement_queued || skip) return; + if (this.#decrement_queued || skip) { + return; + } + this.#decrement_queued = true; queue_micro_task(() => { @@ -556,6 +562,8 @@ export class Batch { } apply() { + this.#blockers.clear(); + if (!async_mode_flag || (!this.is_fork && batches.size === 1)) { batch_values = null; return; @@ -575,18 +583,19 @@ export class Batch { continue; } - var coincides = false; + var intersects = false; + var differs = false; if (is_earlier) { for (const source of batch.current.keys()) { - if (this.current.has(source)) { - coincides = true; - break; - } + intersects ||= this.current.has(source); + differs ||= !this.current.has(source); } } - if (!coincides) { + if (intersects && differs) { + this.#blockers.add(batch); + } else { for (const [source, previous] of batch.previous) { if (!batch_values.has(source)) { batch_values.set(source, previous); From 95a0bec7c2deca8b1c02de56122919ab0ceb6630 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 20:26:23 -0400 Subject: [PATCH 10/26] holy shit it woooooorks --- .../src/internal/client/reactivity/batch.js | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d29eae450c..6885c9118d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -387,6 +387,19 @@ export class Batch { batch_values = null; } + #revive() { + for (const e of this.#dirty_effects) { + this.#maybe_dirty_effects.delete(e); + set_signal_status(e, DIRTY); + this.schedule(e); + } + + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + this.schedule(e); + } + } + flush() { var source_stacks = DEV ? new Set() : null; @@ -397,16 +410,7 @@ export class Batch { // we only reschedule previously-deferred effects if we expect // to be able to run them after processing the batch if (!this.#is_deferred()) { - for (const e of this.#dirty_effects) { - this.#maybe_dirty_effects.delete(e); - set_signal_status(e, DIRTY); - this.schedule(e); - } - - for (const e of this.#maybe_dirty_effects) { - set_signal_status(e, MAYBE_DIRTY); - this.schedule(e); - } + this.#revive(); } this.#process(); @@ -487,12 +491,23 @@ export class Batch { batch.#traverse(root, [], []); } - // TODO do we need to do anything with the dummy effect arrays? + batch.#roots.length = 0; } batch.deactivate(); } } + + for (const batch of batches) { + if (batch.#blockers.has(this)) { + batch.#blockers.delete(this); + + if (batch.#blockers.size === 0 && !batch.#is_deferred()) { + batch.#revive(); + batch.#process(); + } + } + } } /** @@ -562,8 +577,6 @@ export class Batch { } apply() { - this.#blockers.clear(); - if (!async_mode_flag || (!this.is_fork && batches.size === 1)) { batch_values = null; return; From 40f05ab9d342d925c787d1e1b921249ea42dedaa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 20:28:06 -0400 Subject: [PATCH 11/26] update changeset --- .changeset/spicy-teeth-tan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spicy-teeth-tan.md b/.changeset/spicy-teeth-tan.md index 4497392a9f..c7efa65130 100644 --- a/.changeset/spicy-teeth-tan.md +++ b/.changeset/spicy-teeth-tan.md @@ -2,4 +2,4 @@ 'svelte': patch --- -fix: preserve each items that are needed by pending batches +fix: defer batch resolution until earlier intersecting batches have committed From 2c2bc766e9a85eb6acde5337be3946196d7b072a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 21:32:34 -0400 Subject: [PATCH 12/26] WIP --- packages/svelte/src/reactivity/set.js | 81 +++++++++++++-------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index d7c2deeaae..9e5e6ab6a2 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -2,7 +2,8 @@ import { DEV } from 'esm-env'; import { source, set, state, increment } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get, update_version } from '../internal/client/runtime.js'; +import { active_effect, get, untrack, update_version } from '../internal/client/runtime.js'; +import { async_mode_flag } from '../internal/flags/index.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; @@ -47,8 +48,8 @@ var inited = false; export class SvelteSet extends Set { /** @type {Map>} */ #sources = new Map(); - #version = state(0); - #size = state(0); + /** @type {Source>} */ + #items = state(new Set()); #update_version = update_version || -1; /** @@ -57,19 +58,10 @@ export class SvelteSet extends Set { constructor(value) { super(); - if (DEV) { - // If the value is invalid then the native exception will fire here - value = new Set(value); - - tag(this.#version, 'SvelteSet version'); - tag(this.#size, 'SvelteSet.size'); - } + this.#items = state(new Set(value)) - if (value) { - for (var element of value) { - super.add(element); - } - this.#size.v = super.size; + if (DEV) { + tag(this.#items, 'SvelteSet items'); } if (!inited) this.#init(); @@ -98,7 +90,7 @@ export class SvelteSet extends Set { for (const method of read_methods) { // @ts-ignore proto[method] = function (...v) { - get(this.#version); + get(this.#items); // @ts-ignore return set_proto[method].apply(this, v); }; @@ -107,7 +99,7 @@ export class SvelteSet extends Set { for (const method of set_like_methods) { // @ts-ignore proto[method] = function (...v) { - get(this.#version); + get(this.#items); // @ts-ignore var set = /** @type {Set} */ (set_proto[method].apply(this, v)); return new SvelteSet(set); @@ -117,19 +109,18 @@ export class SvelteSet extends Set { /** @param {T} value */ has(value) { - var has = super.has(value); var sources = this.#sources; var s = sources.get(value); if (s === undefined) { - if (!has) { + if (active_effect === null && (!async_mode_flag || !untrack(() => get(this.#items)).has(value))) { // If the value doesn't exist, track the version in case it's added later // but don't create sources willy-nilly to track all possible values - get(this.#version); + get(this.#items); return false; } - s = this.#source(true); + s = this.#source((get(this.#items)).has(value)); if (DEV) { tag(s, `SvelteSet has(${label(value)})`); @@ -138,16 +129,22 @@ export class SvelteSet extends Set { sources.set(value, s); } - get(s); - return has; + return get(s); } /** @param {T} value */ add(value) { - if (!super.has(value)) { - super.add(value); - set(this.#size, super.size); - increment(this.#version); + if (!get(this.#items).has(value)) { + const clone = new Set(get(this.#items)); + clone.add(value); + + set(this.#items, clone); + } + + var s = this.#sources.get(value); + + if (s !== undefined) { + set(s, true); } return this; @@ -155,7 +152,7 @@ export class SvelteSet extends Set { /** @param {T} value */ delete(value) { - var deleted = super.delete(value); + var has = get(this.#items).has(value); var sources = this.#sources; var s = sources.get(value); @@ -164,20 +161,24 @@ export class SvelteSet extends Set { set(s, false); } - if (deleted) { - set(this.#size, super.size); - increment(this.#version); + if (has) { + const clone = new Set(get(this.#items)); + clone.delete(value); + + set(this.#items, clone); } - return deleted; + return has; } clear() { - if (super.size === 0) { + if (get(this.#items).size === 0) { return; } + // Clear first, so we get nice console.log outputs with $inspect - super.clear(); + set(this.#items, new Set()); + var sources = this.#sources; for (var s of sources.values()) { @@ -185,8 +186,6 @@ export class SvelteSet extends Set { } sources.clear(); - set(this.#size, 0); - increment(this.#version); } keys() { @@ -194,20 +193,18 @@ export class SvelteSet extends Set { } values() { - get(this.#version); - return super.values(); + return get(this.#items).values(); } entries() { - get(this.#version); - return super.entries(); + return get(this.#items).entries(); } [Symbol.iterator]() { - return this.keys(); + return this.values(); } get size() { - return get(this.#size); + return get(this.#items).size; } } From 3cdeda6ca918e20601fef5ff0256dfac8063634b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 21:34:12 -0400 Subject: [PATCH 13/26] fix --- packages/svelte/src/internal/client/reactivity/batch.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6885c9118d..c5212ff286 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -503,6 +503,7 @@ export class Batch { batch.#blockers.delete(this); if (batch.#blockers.size === 0 && !batch.#is_deferred()) { + batch.activate(); batch.#revive(); batch.#process(); } From a5b7ac869e6a1444e19bdee1192015ac19787160 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 21:43:55 -0400 Subject: [PATCH 14/26] WIP test --- .../async-overlapping-svelte-set/_config.js | 9 ++++ .../async-overlapping-svelte-set/main.svelte | 43 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js new file mode 100644 index 0000000000..c7e549c429 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [add, shift, pop] = target.querySelectorAll('button'); + + // TODO + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte new file mode 100644 index 0000000000..56eac65192 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte @@ -0,0 +1,43 @@ + + + + + + +

+ pending={$effect.pending()} + values.length={values.size} + values=[{Array.from(values)}] +

+ +
+ +

1: {values.has(1)}

+

2: {values.has(2)}

+

3: {values.has(3)}

+

4: {values.has(4)}

+

5: {values.has(5)}

+ +
+ +{#each values as v(v)} +

{await push(v)}

+{/each} From 839e6786b12aa37354f612bed6c4d9be74bed2b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 21:45:23 -0400 Subject: [PATCH 15/26] rename test --- .../{async-each-overlap => async-overlapping-array}/_config.js | 0 .../{async-each-overlap => async-overlapping-array}/main.svelte | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/svelte/tests/runtime-runes/samples/{async-each-overlap => async-overlapping-array}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{async-each-overlap => async-overlapping-array}/main.svelte (100%) diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js rename to packages/svelte/tests/runtime-runes/samples/async-overlapping-array/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte rename to packages/svelte/tests/runtime-runes/samples/async-overlapping-array/main.svelte From 9baf459b453e0174768b6da238fede647854fe88 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 22:00:26 -0400 Subject: [PATCH 16/26] update test --- .../async-overlapping-svelte-set/_config.js | 138 +++++++++++++++++- .../async-overlapping-svelte-set/main.svelte | 2 +- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js index c7e549c429..e28ba86489 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/_config.js @@ -1,9 +1,145 @@ +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ async test({ assert, target }) { + await tick(); + const [add, shift, pop] = target.querySelectorAll('button'); - // TODO + add.click(); + await tick(); + add.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=2 values.size=1 values=[1]

+
+

1: true

+

2: false

+

3: false

+

4: false

+

5: false

+
+

1

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=1 values.size=2 values=[1,2]

+
+

1: true

+

2: true

+

3: false

+

4: false

+

5: false

+
+

1

+

2

+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.size=3 values=[1,2,3]

+
+

1: true

+

2: true

+

3: true

+

4: false

+

5: false

+
+

1

+

2

+

3

+ ` + ); + + add.click(); + await tick(); + add.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=2 values.size=3 values=[1,2,3]

+
+

1: true

+

2: true

+

3: true

+

4: false

+

5: false

+
+

1

+

2

+

3

+ ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=1 values.size=3 values=[1,2,3]

+
+

1: true

+

2: true

+

3: true

+

4: false

+

5: false

+
+

1

+

2

+

3

+ ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.size=5 values=[1,2,3,4,5]

+
+

1: true

+

2: true

+

3: true

+

4: true

+

5: true

+
+

1

+

2

+

3

+

4

+

5

+ ` + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte index 56eac65192..5995a44990 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-svelte-set/main.svelte @@ -24,7 +24,7 @@

pending={$effect.pending()} - values.length={values.size} + values.size={values.size} values=[{Array.from(values)}]

From 6af7f9068c7879df8c8f3a312453c0162999f66b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 22:00:49 -0400 Subject: [PATCH 17/26] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6885c9118d..255751c324 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -503,6 +503,7 @@ export class Batch { batch.#blockers.delete(this); if (batch.#blockers.size === 0 && !batch.#is_deferred()) { + batch.activate(); batch.#revive(); batch.#process(); } @@ -527,9 +528,7 @@ export class Batch { this.#pending -= 1; if (blocking) this.#blocking_pending -= 1; - if (this.#decrement_queued || skip) { - return; - } + if (this.#decrement_queued || skip) return; this.#decrement_queued = true; From c313797422a1f014674ccc3cd8b5671f83affeb0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 22:03:42 -0400 Subject: [PATCH 18/26] tweak --- .../svelte/src/internal/client/reactivity/batch.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 255751c324..fe759e19d0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -585,20 +585,14 @@ export class Batch { // we need to override values with the ones in this batch... batch_values = new Map(this.current); - var is_earlier = true; - - // ...and undo changes belonging to other batches - // that don't coincide with this batch + // ...and undo changes belonging to other batches unless they block this one for (const batch of batches) { - if (batch === this) { - is_earlier = false; - continue; - } + if (batch === this) continue; var intersects = false; var differs = false; - if (is_earlier) { + if (batch.id < this.id) { for (const source of batch.current.keys()) { intersects ||= this.current.has(source); differs ||= !this.current.has(source); From 3b293191400b9d187a64bce852da2455073137c6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 22:04:27 -0400 Subject: [PATCH 19/26] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fe759e19d0..a4aaf34a1c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -529,7 +529,6 @@ export class Batch { if (blocking) this.#blocking_pending -= 1; if (this.#decrement_queued || skip) return; - this.#decrement_queued = true; queue_micro_task(() => { From cbd804bc856c8cb05f55bf1ab132f1390fa31503 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 22:04:51 -0400 Subject: [PATCH 20/26] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fe759e19d0..a4aaf34a1c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -529,7 +529,6 @@ export class Batch { if (blocking) this.#blocking_pending -= 1; if (this.#decrement_queued || skip) return; - this.#decrement_queued = true; queue_micro_task(() => { From cfc47b565431839eabb54d2c9cd87e8f4ee531e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Mar 2026 22:16:54 -0400 Subject: [PATCH 21/26] changeset --- .changeset/async-svelte-set.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/async-svelte-set.md diff --git a/.changeset/async-svelte-set.md b/.changeset/async-svelte-set.md new file mode 100644 index 0000000000..6f66f950d4 --- /dev/null +++ b/.changeset/async-svelte-set.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make SvelteSet work with async mode From fb7f1eeb5d062e4bb7cc403d2095f80a1cb973fd Mon Sep 17 00:00:00 2001 From: Vercel Date: Wed, 11 Mar 2026 13:03:30 +0000 Subject: [PATCH 22/26] Fix: SvelteSet's read_methods (forEach, isDisjointFrom, isSubsetOf, isSupersetOf) and set_like_methods (difference, intersection, symmetricDifference, union) operate on an empty underlying Set instead of the actual data stored in #items. This commit fixes the issue reported at packages/svelte/src/reactivity/set.js:92 **Bug Explanation:** After the refactoring to make SvelteSet async-safe, the data storage changed: - **Before:** Data was stored in the underlying Set via `super.add()` in the constructor - **After:** Data is stored in a reactive `#items` source containing a Set, while `super()` is called without arguments (empty Set) The `#init()` method creates implementations for `read_methods` and `set_like_methods` by delegating to `Set.prototype` methods. However, the code used `set_proto[method].apply(this, v)` where `this` is the SvelteSet instance. Since SvelteSet extends Set but never adds items to itself (all data is in `#items`), calling these Set methods on `this` operates on an empty Set. This caused severe bugs: - `forEach()` would iterate over nothing instead of actual items - `isSubsetOf()` would always return `true` (empty set is subset of everything) - `isSupersetOf()` would always return `false` (empty set can't be superset of non-empty) - `union()` would only return the other set (union with empty set) - `intersection()` would return empty (intersection with empty set) - `difference()` would return empty - `symmetricDifference()` would just return the other set's items **The Fix:** Changed `set_proto[method].apply(this, v)` to `set_proto[method].apply(get(this.#items), v)` in both loops. This ensures the Set methods operate on the actual data stored in `#items` (the reactive Set that contains all the items) rather than the empty underlying Set that the SvelteSet class extends. The fix maintains reactivity since `get(this.#items)` is still called, establishing the reactive dependency as intended. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/svelte/src/reactivity/set.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 9e5e6ab6a2..18e7641f52 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -90,18 +90,16 @@ export class SvelteSet extends Set { for (const method of read_methods) { // @ts-ignore proto[method] = function (...v) { - get(this.#items); // @ts-ignore - return set_proto[method].apply(this, v); + return set_proto[method].apply(get(this.#items), v); }; } for (const method of set_like_methods) { // @ts-ignore proto[method] = function (...v) { - get(this.#items); // @ts-ignore - var set = /** @type {Set} */ (set_proto[method].apply(this, v)); + var set = /** @type {Set} */ (set_proto[method].apply(get(this.#items), v)); return new SvelteSet(set); }; } From b5cece5693449789a396c834bc807026b24a7f86 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Mar 2026 10:12:40 -0400 Subject: [PATCH 23/26] WIP --- packages/svelte/src/reactivity/set.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 18e7641f52..0ce3f38d43 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, increment } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_effect, get, untrack, update_version } from '../internal/client/runtime.js'; +import { active_effect, active_reaction, get, untrack, update_version } from '../internal/client/runtime.js'; import { async_mode_flag } from '../internal/flags/index.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; @@ -58,7 +58,13 @@ export class SvelteSet extends Set { constructor(value) { super(); - this.#items = state(new Set(value)) + this.#items = state(new Set(value)); + + var sources = this.#sources; + + for (const value of this.#items.v) { + sources.set(value, this.#source(true)); + } if (DEV) { tag(this.#items, 'SvelteSet items'); @@ -111,7 +117,7 @@ export class SvelteSet extends Set { var s = sources.get(value); if (s === undefined) { - if (active_effect === null && (!async_mode_flag || !untrack(() => get(this.#items)).has(value))) { + if (!async_mode_flag) { // If the value doesn't exist, track the version in case it's added later // but don't create sources willy-nilly to track all possible values get(this.#items); @@ -139,10 +145,13 @@ export class SvelteSet extends Set { set(this.#items, clone); } - var s = this.#sources.get(value); + var sources = this.#sources; + var s = sources.get(value); if (s !== undefined) { set(s, true); + } else { + sources.set(value, this.#source(true)); } return this; From dd2f22d75bac9aa065f6828d31b3608ff68930ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Mar 2026 10:24:37 -0400 Subject: [PATCH 24/26] fix, tidy up --- packages/svelte/src/reactivity/set.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 0ce3f38d43..9b5889872e 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,8 +1,8 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { source, set, state, increment } from '../internal/client/reactivity/sources.js'; +import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { active_effect, active_reaction, get, untrack, update_version } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { async_mode_flag } from '../internal/flags/index.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; @@ -138,13 +138,6 @@ export class SvelteSet extends Set { /** @param {T} value */ add(value) { - if (!get(this.#items).has(value)) { - const clone = new Set(get(this.#items)); - clone.add(value); - - set(this.#items, clone); - } - var sources = this.#sources; var s = sources.get(value); @@ -154,6 +147,13 @@ export class SvelteSet extends Set { sources.set(value, this.#source(true)); } + if (!get(this.#items).has(value)) { + const clone = new Set(get(this.#items)); + clone.add(value); + + set(this.#items, clone); + } + return this; } From df4070d3eb43d0501d6ce2fceb834740892ba9d8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Mar 2026 10:29:25 -0400 Subject: [PATCH 25/26] prettier --- packages/svelte/src/reactivity/set.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 9b5889872e..d8603e6308 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -124,7 +124,7 @@ export class SvelteSet extends Set { return false; } - s = this.#source((get(this.#items)).has(value)); + s = this.#source(get(this.#items).has(value)); if (DEV) { tag(s, `SvelteSet has(${label(value)})`); From f2030bba2922507d4fd46b8180ebd377bd22e449 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Mar 2026 14:52:44 -0400 Subject: [PATCH 26/26] tidy up --- packages/svelte/src/reactivity/set.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index d8603e6308..7f6adad047 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -49,7 +49,7 @@ export class SvelteSet extends Set { /** @type {Map>} */ #sources = new Map(); /** @type {Source>} */ - #items = state(new Set()); + #items; #update_version = update_version || -1; /** @@ -147,8 +147,10 @@ export class SvelteSet extends Set { sources.set(value, this.#source(true)); } - if (!get(this.#items).has(value)) { - const clone = new Set(get(this.#items)); + var items = get(this.#items); + + if (!items.has(value)) { + const clone = new Set(items); clone.add(value); set(this.#items, clone); @@ -159,7 +161,8 @@ export class SvelteSet extends Set { /** @param {T} value */ delete(value) { - var has = get(this.#items).has(value); + var items = get(this.#items); + var has = items.has(value); var sources = this.#sources; var s = sources.get(value); @@ -169,7 +172,7 @@ export class SvelteSet extends Set { } if (has) { - const clone = new Set(get(this.#items)); + const clone = new Set(items); clone.delete(value); set(this.#items, clone);