From 0eab02db24cadb74e29e4cdf1bb92fbef206e483 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 May 2026 21:44:12 -0400 Subject: [PATCH 1/8] chore: flatten `#process()` --- .../src/internal/client/reactivity/batch.js | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8f14cb4437df..7c9e0a6df41a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -334,26 +334,28 @@ export class Batch { for (const [e, t] of this.#skipped_branches) { reset_branch(e, t); } - } else { - if (this.#pending === 0) { - batches.delete(this); - } - // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. - this.#dirty_effects.clear(); - this.#maybe_dirty_effects.clear(); + return; + } + + if (this.#pending === 0) { + batches.delete(this); + } - // append/remove branches - for (const fn of this.#commit_callbacks) fn(this); - this.#commit_callbacks.clear(); + // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. + this.#dirty_effects.clear(); + this.#maybe_dirty_effects.clear(); - previous_batch = this; - flush_queued_effects(render_effects); - flush_queued_effects(effects); - previous_batch = null; + // append/remove branches + for (const fn of this.#commit_callbacks) fn(this); + this.#commit_callbacks.clear(); - this.#deferred?.resolve(); - } + previous_batch = this; + flush_queued_effects(render_effects); + flush_queued_effects(effects); + previous_batch = null; + + this.#deferred?.resolve(); var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); From 4a06bc5d15e30a2ac37c25cf4c23dec58fc83f52 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 May 2026 21:49:37 -0400 Subject: [PATCH 2/8] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7c9e0a6df41a..414af6a70157 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -338,10 +338,6 @@ export class Batch { return; } - if (this.#pending === 0) { - batches.delete(this); - } - // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. this.#dirty_effects.clear(); this.#maybe_dirty_effects.clear(); @@ -363,7 +359,7 @@ export class Batch { // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong. // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed - if (async_mode_flag && !batches.has(this)) { + if (async_mode_flag && this.#pending === 0) { this.#commit(); // Rebases can activate other batches or null it out, therefore restore the new one here current_batch = next_batch; @@ -527,6 +523,8 @@ export class Batch { } #commit() { + batches.delete(this); + // If there are other pending batches, they now need to be 'rebased' — // in other words, we re-run block/async effects with the newly // committed state, unless the batch in question has a more From 366ae11c41df1b9ec42b31da13d21e26c8d98066 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 8 May 2026 10:14:41 -0400 Subject: [PATCH 3/8] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 414af6a70157..21cd46470a46 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -335,7 +335,9 @@ export class Batch { reset_branch(e, t); } - return; + if (updates.length === 0) { + return; + } } // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. From e527e1e8acbfab3718825b5146f89420ad35ae31 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 8 May 2026 10:33:16 -0400 Subject: [PATCH 4/8] test --- .../_config.js | 52 +++++++++++++++++++ .../main.svelte | 27 ++++++++++ 2 files changed, 79 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js new file mode 100644 index 000000000000..4a36e05b8c4b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +// Test that the store is unsubscribed from, even if it's not referenced once the store itself is set to null +export default test({ + async test({ target, assert }) { + assert.htmlEqual( + target.innerHTML, + `

0

` + ); + + target.querySelector('button')?.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

1

hello 1 ` + ); + + const input = target.querySelector('input'); + ok(input); + + input.stepUp(); + input.dispatchEvent(new Event('input', { bubbles: true })); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

2

hello 2 ` + ); + + target.querySelector('button')?.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

2

` + ); + + input.stepUp(); + input.dispatchEvent(new Event('input', { bubbles: true })); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

2

` + ); + + target.querySelector('button')?.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

3

hello 3 ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte new file mode 100644 index 000000000000..b75d19d2f57c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte @@ -0,0 +1,27 @@ + + + +

{count}

+ +{#if watcherA} + + {await 'hello'} + + {$watcherA} + +{:else} + +{/if} From 5bc6b56981229fdd4d55250a6121b0e45867e9b2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 8 May 2026 10:35:23 -0400 Subject: [PATCH 5/8] ok that seems unnecessary too? wtf --- packages/svelte/src/internal/client/reactivity/batch.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 21cd46470a46..414af6a70157 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -335,9 +335,7 @@ export class Batch { reset_branch(e, t); } - if (updates.length === 0) { - return; - } + return; } // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. From 804d223f1538d767a7a3a752b46d1cffaf02ec6c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 8 May 2026 10:38:39 -0400 Subject: [PATCH 6/8] fix the test, so that it fails correctly --- .../store-unsubscribe-not-referenced-after-2/main.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte index b75d19d2f57c..4abfd3c2f933 100644 --- a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/main.svelte @@ -18,7 +18,9 @@ {#if watcherA} - {await 'hello'} + {#if true} + {await 'hello'} + {/if} {$watcherA} From 2d6078eb8cde320714592bf8a164319fc784681b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 8 May 2026 10:47:51 -0400 Subject: [PATCH 7/8] okay i think i got it --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++++ .../store-unsubscribe-not-referenced-after-2/_config.js | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 414af6a70157..2a3609b90ecf 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -335,6 +335,10 @@ export class Batch { reset_branch(e, t); } + if (updates.length > 0) { + /** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process(); + } + return; } diff --git a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js index 4a36e05b8c4b..b3ec9b200d19 100644 --- a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js @@ -3,6 +3,8 @@ import { ok, test } from '../../test'; // Test that the store is unsubscribed from, even if it's not referenced once the store itself is set to null export default test({ + skip_async: true, + async test({ target, assert }) { assert.htmlEqual( target.innerHTML, From 719ea360a3fdfc5c136011b2e138afa21c0ee33d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 8 May 2026 10:50:24 -0400 Subject: [PATCH 8/8] doh --- .../samples/store-unsubscribe-not-referenced-after-2/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js index b3ec9b200d19..d7293f9b70df 100644 --- a/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/store-unsubscribe-not-referenced-after-2/_config.js @@ -3,7 +3,7 @@ import { ok, test } from '../../test'; // Test that the store is unsubscribed from, even if it's not referenced once the store itself is set to null export default test({ - skip_async: true, + skip_no_async: true, async test({ target, assert }) { assert.htmlEqual(