From 3f7bdda5ce1e5760227b9c8a6f8dd15a335e53b9 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 30 Apr 2026 22:12:00 +0200 Subject: [PATCH] fix: use right batch/branch on first run Fixes the regression that was uncovered by a SvelteKit test. It consistens of two parts: 1. Restoring the latest, not the initial batch in `flatten`: At the beginning `flatten` stores the current batch, and once everything async is finished it restores it. That falls down if one of the async deriveds reruns during that time. Now the batch associated with that rerun should be used because its `current` map contains a later value. We gotta use that. The hacky fix for now is to set the latest batch onto the source that the async derived has. That could mess with perf since the source object shape is not the same all the time anymore. There's probably a better way to write this but it's getting late here. This fix solves the regression part where it shows the wrong string for the url pathname. 2. branches deletes older batches and then bails when it comes across itself. The logic assumes that batches are in the map in ascending order but that's not always true. Making the logic robust to that fixes the part where it keeps showing the "should never see this" string from the obsolete `+page.svelte`. I wasn't able to make a test for this yet. --- .changeset/solid-banks-teach.md | 5 ++++ .../internal/client/dom/blocks/branches.js | 7 +++-- .../src/internal/client/reactivity/async.js | 27 ++++++++++++++++++- .../internal/client/reactivity/deriveds.js | 1 + .../src/internal/client/reactivity/types.d.ts | 3 +++ .../_config.js | 20 ++++++++++++++ .../main.svelte | 26 ++++++++++++++++++ 7 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 .changeset/solid-banks-teach.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/main.svelte diff --git a/.changeset/solid-banks-teach.md b/.changeset/solid-banks-teach.md new file mode 100644 index 0000000000..eb16d051b2 --- /dev/null +++ b/.changeset/solid-banks-teach.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use right batch/branch on first run diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 33c34f58bb..3732bc2de2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -109,11 +109,14 @@ export class BranchManager { } for (const [b, k] of this.#batches) { + // Keep values for newer batches. Insertion order is not always chronological: + // an older batch can re-run after a newer one has already registered. + if (b.id > batch.id) continue; + this.#batches.delete(b); if (b === batch) { - // keep values for newer batches - break; + continue; } const offscreen = this.#offscreen.get(k); diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 6aea790c36..549141dbfb 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -55,7 +55,14 @@ export function flatten(blockers, sync, async, fn) { /** @param {Value[]} values */ function finish(values) { - restore(); + var batch = get_latest_async_batch(values); + if (batch) { + restore(false); + batch.activate(); + batch.apply(); + } else { + restore(); + } try { fn(values); @@ -95,6 +102,24 @@ export function flatten(blockers, sync, async, fn) { } } +/** + * @param {Value[]} values + * @returns {Batch | null} + */ +function get_latest_async_batch(values) { + /** @type {Batch | null} */ + var latest = null; + + for (const value of values) { + var batch = /** @type {Value & { async_batch?: Batch }} */ (value).async_batch; + if (batch && (!latest || batch.id > latest.id)) { + latest = batch; + } + } + + return latest; +} + /** * @param {Blocker[]} blockers * @param {(values: Value[]) => any} fn diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 4ae49fecba..b4ee0ce8a0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -215,6 +215,7 @@ export function async_derived(fn, label, location) { } batch.activate(); + /** @type {Source & { async_batch?: Batch }} */ (signal).async_batch = batch; if (error) { signal.f |= ERROR_VALUE; diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 0ee8570c3d..1f41e1fca6 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -7,6 +7,7 @@ import type { TransitionManager } from '#client'; import type { Boundary } from '../dom/blocks/boundary'; +import type { Batch } from './batch'; export interface Signal { /** Flags bitmask */ @@ -24,6 +25,8 @@ export interface Value extends Signal { rv: number; /** The latest value for this signal */ v: V; + /** The batch in which an async derived most recently resolved */ + async_batch?: Batch; // TODO if this is only set a few times this might mess with perf (object shape etc) // dev-only /** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/_config.js b/packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/_config.js new file mode 100644 index 0000000000..c8bc4c986f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` 2 2 1`); + + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` 2 2 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/main.svelte new file mode 100644 index 0000000000..7689af049c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-new-batch-during-initial-load/main.svelte @@ -0,0 +1,26 @@ + + + + + +{#if count > 0} + + {await push(count)} {count} {other} + {#snippet failed()}boom{/snippet} + +{/if}