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..8a1f4666ec 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -113,20 +113,30 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - /** @type {Map>>} */ + /** @type {Map> & { rejected?: boolean }>} */ var deferreds = new Map(); async_effect(() => { if (DEV) current_async_effect = active_effect; - /** @type {ReturnType>} */ + /** @type {ReturnType> & { rejected?: boolean }} */ var d = deferred(); promise = d.promise; 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); + Promise.resolve(fn()).then((v) => { + if (d.rejected) { + // If we rejected this stale promise, d.resolve + // is a noop (d.promise.then(handler) below will never run). + // In this case we need to unset the restored context here + // to avoid leaking it (and e.g. cause false-positive mutation errors). + unset_context(); + } else { + d.resolve(v); + } + }, d.reject); } catch (error) { d.reject(error); } @@ -141,7 +151,11 @@ export function async_derived(fn, location) { if (!pending) { batch.increment(); - deferreds.get(batch)?.reject(STALE_REACTION); + var previous_deferred = deferreds.get(batch); + if (previous_deferred) { + previous_deferred.rejected = true; + previous_deferred.reject(STALE_REACTION); + } deferreds.set(batch, d); } } 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..945d52ba5d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte @@ -0,0 +1,38 @@ + + + + +{count} | {x} \ No newline at end of file