diff --git a/.changeset/many-jobs-speak.md b/.changeset/many-jobs-speak.md new file mode 100644 index 000000000000..717e7ebd0468 --- /dev/null +++ b/.changeset/many-jobs-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure async batches get noticed of new effects diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 57aa185a31db..639ff36e52e3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -229,8 +229,65 @@ export class Batch { } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { target.render_effects.push(effect); } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); - update_effect(effect); + if ((effect.f & BLOCK_EFFECT) !== 0) { + target.block_effects.push(effect); + + let branches; + let has_multiple_batches = batches.size > 1; + if (has_multiple_batches) { + branches = new Set(); + let b = effect.first; + + while (b !== null) { + if ((b.f & BRANCH_EFFECT) !== 0) { + branches.add(b); + } + b = b.next; + } + } + + update_effect(effect); + + let new_branches; + if (has_multiple_batches) { + new_branches = new Set(); + let b = effect.first; + + while (b !== null) { + const next = b.next; + if (!(/** @type {Set} */ (branches).has(b))) { + new_branches.add(b); + } + b = next; + } + + const new_target = { + parent: null, + effect, + effects: [], + render_effects: [], + block_effects: [] + }; + + // Traverse any new branches added due to running the block effect and collect their effects... + if (new_branches.size > 0) { + for (const b of new_branches) { + this.#traverse_new_effects(b, new_target); + } + } + + // ...then defer those effects in other batches, as they could have changed values that these effects depend on + for (const b of batches) { + if (b !== this) { + b.#defer_effects(new_target.render_effects, false); + b.#defer_effects(new_target.effects, false); + b.#defer_effects(new_target.block_effects, false); + } + } + } + } else { + update_effect(effect); + } } var child = effect.first; @@ -262,16 +319,57 @@ export class Batch { } } + /** + * Traverse the newly created effect tree, adding effects to the appropriate lists + * @param {Effect} root + * @param {EffectTarget} target + */ + #traverse_new_effects(root, target) { + var effect = root.first; + + while (effect !== null) { + var flags = effect.f; + if (effect.fn !== null) { + if ((flags & EFFECT) !== 0) { + target.effects.push(effect); + } else if ((flags & RENDER_EFFECT) !== 0) { + target.render_effects.push(effect); + } else if ((effect.f & BLOCK_EFFECT) !== 0) { + target.block_effects.push(effect); + } + + var child = effect.first; + + if (child !== null) { + effect = child; + continue; + } + } + + var parent = effect.parent; + effect = effect.next; + + while (effect === null && parent !== null && parent !== root) { + effect = parent.next; + parent = parent.parent; + } + } + } + /** * @param {Effect[]} effects + * @param {boolean} use_status */ - #defer_effects(effects) { + #defer_effects(effects, use_status = true) { for (const e of effects) { - const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; + const target = + (e.f & DIRTY) !== 0 && use_status ? this.#dirty_effects : this.#maybe_dirty_effects; target.push(e); - // mark as clean so they get scheduled if they depend on pending async state - set_signal_status(e, CLEAN); + if (use_status) { + // mark as clean so they get scheduled if they depend on pending async state + set_signal_status(e, CLEAN); + } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/Child.svelte new file mode 100644 index 000000000000..6b765526c82f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/Child.svelte @@ -0,0 +1,7 @@ + + +{x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js new file mode 100644 index 000000000000..f4b6cc777bde --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + assert.deepEqual(logs, ['universe']); + + y.click(); + await tick(); + assert.deepEqual(logs, ['universe', 'world', '$effect: world']); + assert.htmlEqual( + target.innerHTML, + ` + + + + world + ` + ); + + resolve.click(); + await tick(); + assert.deepEqual(logs, [ + 'universe', + 'world', + '$effect: world', + '$effect: universe', + '$effect: universe' + ]); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + universe + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/main.svelte new file mode 100644 index 000000000000..8edc718de2ae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/main.svelte @@ -0,0 +1,28 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +{#if y > 0} + +{/if}