From 2deebdea8ffbdb74790ce7021e3b6992b39b77bb Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 7 Mar 2026 01:59:58 +0100 Subject: [PATCH] fix: handle asnyc updates within pending boundary (#17873) When an async value is updated inside the boundary while the pending snippet is shown, we previously didn't notice that update and instead showed an outdated value once it resolved. This fixes that by rejecting all deferreds inside an async_derived while the pending snippet is shown. --------- Co-authored-by: Rich Harris --- .changeset/nasty-friends-crash.md | 5 ++ .../internal/client/reactivity/deriveds.js | 14 ++++- .../_config.js | 51 +++++++++++++++++++ .../main.svelte | 19 +++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 .changeset/nasty-friends-crash.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/main.svelte diff --git a/.changeset/nasty-friends-crash.md b/.changeset/nasty-friends-crash.md new file mode 100644 index 0000000000..5895f3752a --- /dev/null +++ b/.changeset/nasty-friends-crash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle asnyc updates within pending boundary diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c1ee4f3f52..d8989ef03d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -146,8 +146,18 @@ export function async_derived(fn, label, location) { if (should_suspend) { var decrement_pending = increment_pending(); - deferreds.get(batch)?.reject(STALE_REACTION); - deferreds.delete(batch); // delete to ensure correct order in Map iteration below + if (/** @type {Boundary} */ (parent.b).is_rendered()) { + deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.delete(batch); // delete to ensure correct order in Map iteration below + } else { + // While the boundary is still showing pending, a new run supersedes all older in-flight runs + // for this async expression. Cancel eagerly so resolution cannot commit stale values. + for (const d of deferreds.values()) { + d.reject(STALE_REACTION); + } + deferreds.clear(); + } + deferreds.set(batch, d); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/_config.js new file mode 100644 index 0000000000..e444aa8f9b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [shift, increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + loading + ` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + loading + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + loading + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + 1 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/main.svelte new file mode 100644 index 0000000000..c5a32dc4b9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-update-while-pending/main.svelte @@ -0,0 +1,19 @@ + + + + + + + {await push(count)} + {#snippet pending()}loading{/snippet} +