diff --git a/.changeset/tame-ears-invite.md b/.changeset/tame-ears-invite.md new file mode 100644 index 000000000000..05670a0add38 --- /dev/null +++ b/.changeset/tame-ears-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure obsolete batches are removed and its necessary dom changes committed diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 82f1de67a98e..f1ef636bf775 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,7 +10,6 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT, MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; @@ -96,9 +95,9 @@ export class Batch { /** * When the batch is committed (and the DOM is updated), we need to remove old branches * and append new ones by calling the functions added inside (if/each/key/etc) blocks - * @type {Set<() => void>} + * @type {Map void>} */ - #callbacks = new Set(); + #callbacks = new Map(); /** * The number of async effects that are currently in flight @@ -112,12 +111,6 @@ export class Batch { */ #deferred = null; - /** - * True if an async effect inside this batch resolved and - * its parent branch was already deleted - */ - #neutered = false; - /** * Async effects (created inside `async_derived`) encountered during processing. * These run after the rest of the batch has updated, since they should @@ -197,7 +190,9 @@ export class Batch { } for (const batch of batches) { - if (batch === this) continue; + if (batch === this) { + continue; + } for (const [source, previous] of batch.#previous) { if (!current_values.has(source)) { @@ -215,6 +210,24 @@ export class Batch { // if we didn't start any new async work, and no async work // is outstanding from a previous flush, commit if (this.#async_effects.length === 0 && this.#pending === 0) { + for (const batch of batches) { + if (batch === this) break; + + for (const effects of [batch.#dirty_effects, batch.#maybe_dirty_effects]) { + let i = effects.length; + while (i--) { + // if an effect in an earlier batch only depends on state that + // is part of this batch, we can delete it from the other batch + const effect = effects[i]; + const has_other_deps = effect.deps?.some((value) => !this.current.has(value)); + + if (!has_other_deps) { + effects.splice(i, 1); // TODO should probably be a `Set` if we're going to do this + } + } + } + } + this.#commit(); var render_effects = this.#render_effects; @@ -372,8 +385,17 @@ export class Batch { } } - neuter() { - this.#neutered = true; + remove() { + // Cleanup to + // - prevent memory leaks which could happen if a batch is tied to a never-ending promise + // - prevent effects from rerunning for outdated-and-now-no-longer-pending batches + this.#callbacks.clear(); + this.#maybe_dirty_effects = + this.#dirty_effects = + this.#boundary_async_effects = + this.#async_effects = + []; + batches.delete(this); } flush() { @@ -400,10 +422,8 @@ export class Batch { * Append and remove branches to/from the DOM */ #commit() { - if (!this.#neutered) { - for (const fn of this.#callbacks) { - fn(); - } + for (const fn of this.#callbacks.values()) { + fn(); } this.#callbacks.clear(); @@ -436,9 +456,11 @@ export class Batch { } } - /** @param {() => void} fn */ + /** + * @param {() => void} fn + */ add_callback(fn) { - this.#callbacks.add(fn); + this.#callbacks.set(/** @type {Effect} */ (active_effect), fn); } settled() { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 31dc26796099..f0037f6405b3 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -188,12 +188,6 @@ export function async_derived(fn, location) { }; promise.then(handler, (e) => handler(null, e || 'unknown')); - - if (batch) { - return () => { - queueMicrotask(() => batch.neuter()); - }; - } }); if (DEV) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect-2/_config.js new file mode 100644 index 000000000000..de9b948cd32d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-2/_config.js @@ -0,0 +1,36 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, logs, target }) { + assert.htmlEqual( + target.innerHTML, + ` +

a

+ + + +

a

+ ` + ); + + const [a, b] = target.querySelectorAll('button'); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + +

c

+

b or c

+ ` + ); + + assert.deepEqual(logs, ['route a', 'route c']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect-2/main.svelte new file mode 100644 index 000000000000..eb1e90cd307b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-2/main.svelte @@ -0,0 +1,41 @@ + + +

{route}

+ + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {await goto('c')} + {/if} + + {#if route === 'c'} +

c

+ {/if} + + {#if route === 'b' || route === 'c'} +

b or c

+ {/if} + + {#snippet pending()} +

pending...

+ {/snippet} +