From 652103bb9fcf0036fcb5bb72aa0f5cc06be5969a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Oct 2026 12:07:20 -0400 Subject: [PATCH] restore is_eager early return in #find_earlier_batch An eager batch sees other batches' previous values, so its async results must not resolve an earlier batch's work. It can still get a dependency via the in-flight check in async_derived. --- .../src/internal/client/reactivity/batch.js | 2 + .../async-eager-batch-no-merge/_config.js | 44 +++++++++++++++++++ .../async-eager-batch-no-merge/main.svelte | 20 +++++++++ 3 files changed, 66 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index dd230d3787..f4c70c822f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -609,6 +609,8 @@ export class Batch { } #find_earlier_batch() { + if (this.is_eager) return null; + for (var batch = this.prev; batch !== null; batch = batch.prev) { if (!batch.is_fork && this.dependent.has(batch)) { return batch; diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js new file mode 100644 index 0000000000..f90497846c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js @@ -0,0 +1,44 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// The eager batch that re-runs `load` for the new `$state.eager(filter)` value computes it +// with the old `page`, because eager batches don't see other batches' changes. Therefore it +// must not be merged into the (pending) `page` batch — else its result would resolve that +// batch's `load(2, ...)` and be shown next to `page: 2` +export default test({ + async test({ assert, target }) { + await tick(); + const [page, filter, shift, pop] = target.querySelectorAll('button'); + + // B1: page = 2 -> load(2, 'a') pending + page.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

page: 1

result: 1/a

`); + + // filter = 'b' -> eager batch re-runs load(1, 'b'), pending + filter.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

page: 1

result: 1/a

`); + + // resolve load(1, 'b') -> the eager batch commits on its own + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

page: 1

result: 1/b

`); + + // resolve load(2, 'a') -> B1 commits (showing B1's own result is fine, as is + // waiting for load(2, 'b'), as long as the result matches the page) + shift.click(); + await tick(); + const [p1, p2] = target.querySelectorAll('p'); + assert.equal(p1.textContent, 'page: 2'); + assert.match(p2.textContent ?? '', /^result: 2\//); + + // load(2, 'b') resolves + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

page: 2

result: 2/b

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte new file mode 100644 index 0000000000..008fabe2e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte @@ -0,0 +1,20 @@ + + + + + + + +

page: {page}

+

result: {await load(page, $state.eager(filter))}