From 23e2bb3b89cf27bd4526c59a03cee86df1bd3eb2 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:22:44 +0200 Subject: [PATCH] fix: unset context on stale promises (#16935) * fix: unset context on stale promises When a stale promise is rejected in `async_derived`, and the promise eventually resolves, `d.resolve` will be noop and `d.promise.then(handler, ...)` will never run. That in turns means any restored context (via `(await save(..))()`) will never be unset. We have to handle this case and unset the context to prevent errors such as false-positive state mutation errors * fix: unset context on stale promises (slightly different approach) (#16936) * slightly different approach to #16935 * move unset_context call * get rid of logs --------- Co-authored-by: Rich Harris --- .changeset/major-beans-fry.md | 5 +++ .../internal/client/reactivity/deriveds.js | 6 ++-- .../samples/async-resolve-stale/_config.js | 26 ++++++++++++++ .../samples/async-resolve-stale/main.svelte | 34 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 .changeset/major-beans-fry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte diff --git a/.changeset/major-beans-fry.md b/.changeset/major-beans-fry.md new file mode 100644 index 0000000000..8f35683cd6 --- /dev/null +++ b/.changeset/major-beans-fry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unset context on stale promises diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5d5976a6c1..076a919236 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -126,9 +126,11 @@ export function async_derived(fn, location) { try { // If this code is changed at some point, make sure to still access the then property // of fn() to read any signals it might access, so that we track them as dependencies. - Promise.resolve(fn()).then(d.resolve, d.reject); + // We call `unset_context` to undo any `save` calls that happen inside `fn()` + Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context); } catch (error) { d.reject(error); + unset_context(); } if (DEV) current_async_effect = null; @@ -185,8 +187,6 @@ export function async_derived(fn, location) { boundary.update_pending_count(-1); if (!pending) batch.decrement(); } - - unset_context(); }; d.promise.then(handler, (e) => handler(null, e || 'unknown')); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js new file mode 100644 index 0000000000..bccf12562a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js @@ -0,0 +1,26 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + // We gotta wait a bit more in this test because of the macrotasks in App.svelte + function macrotask(t = 3) { + return new Promise((r) => setTimeout(r, t)); + } + + await macrotask(); + assert.htmlEqual(target.innerHTML, ' 1 | '); + + const [input] = target.querySelectorAll('input'); + + input.value = '1'; + input.dispatchEvent(new Event('input', { bubbles: true })); + await macrotask(); + assert.htmlEqual(target.innerHTML, ' 1 | '); + + input.value = '12'; + input.dispatchEvent(new Event('input', { bubbles: true })); + await macrotask(6); + // TODO this is wrong (separate bug), this should be 3 | 12 + assert.htmlEqual(target.innerHTML, ' 5 | 12'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte new file mode 100644 index 0000000000..dc4a157928 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte @@ -0,0 +1,34 @@ + + + + +{count} | {x}