From 0395ef0df7ff12c4b633b650c5f2db512d382836 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:11:08 +0200 Subject: [PATCH] fix: unskip branches of earlier batches after commit (#18048) Fixes #17571 where the situation is the following: A derived creates a new query. That query initializes loading with true. This means the if block is marked for destruction (therefore effects inside branch are skipped), but it's not doing that yet because the query promise is pending. Then query resolves and loading is set back to false right before resolving, but it's not the same tick so `loading=false` is a separate thing. Because that later batch doesn't see any overlap with an earlier batch (the earlier batch did set loading to true but not via set but indirectly via recreating the query) it doesn't wait on it and flushes right away. Now the if block is marked as visible again but the earlier batch doesn't know that if noone unskips its branch. If we don't do that the render effect that is now dirty as part of that batch will not run. --- .changeset/petite-signs-flash.md | 5 +++ .../src/internal/client/reactivity/batch.js | 28 +++++++++++-- .../samples/async-if-block-unskip/_config.js | 30 ++++++++++++++ .../samples/async-if-block-unskip/main.svelte | 40 +++++++++++++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 .changeset/petite-signs-flash.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte diff --git a/.changeset/petite-signs-flash.md b/.changeset/petite-signs-flash.md new file mode 100644 index 0000000000..38a1c7b47c --- /dev/null +++ b/.changeset/petite-signs-flash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unskip branches of earlier batches after commit diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index f98436e7bc..9c0832150a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -172,6 +172,12 @@ export class Batch { */ #skipped_branches = new Map(); + /** + * Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing + * @type {Set} + */ + #unskipped_branches = new Set(); + is_fork = false; #decrement_queued = false; @@ -215,28 +221,31 @@ export class Batch { if (!this.#skipped_branches.has(effect)) { this.#skipped_branches.set(effect, { d: [], m: [] }); } + this.#unskipped_branches.delete(effect); } /** * Remove an effect from the #skipped_branches map and reschedule * any tracked dirty/maybe_dirty child effects * @param {Effect} effect + * @param {(e: Effect) => void} callback */ - unskip_effect(effect) { + unskip_effect(effect, callback = (e) => this.schedule(e)) { var tracked = this.#skipped_branches.get(effect); if (tracked) { this.#skipped_branches.delete(effect); for (var e of tracked.d) { set_signal_status(e, DIRTY); - this.schedule(e); + callback(e); } for (e of tracked.m) { set_signal_status(e, MAYBE_DIRTY); - this.schedule(e); + callback(e); } } + this.#unskipped_branches.add(effect); } #process() { @@ -532,6 +541,19 @@ export class Batch { invariant(batch.#roots.length === 0, 'Batch has scheduled roots'); } + // A batch was unskipped in a later batch -> tell prior batches to unskip it, too + if (is_earlier) { + for (const unskipped of this.#unskipped_branches) { + batch.unskip_effect(unskipped, (e) => { + if ((e.f & (BLOCK_EFFECT | ASYNC)) !== 0) { + batch.schedule(e); + } else { + batch.#defer_effects([e]); + } + }); + } + } + batch.activate(); /** @type {Set} */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js new file mode 100644 index 0000000000..d578def483 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [load, resolve] = target.querySelectorAll('button'); + + load.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + search search search search + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte new file mode 100644 index 0000000000..36ebcd26d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte @@ -0,0 +1,40 @@ + + +{query} {await promise} + +{#if !promise.loading} + {query} +{/if} + +{#if !promise.loading} + {await query} +{/if} + + +