fix: unset context synchronously in `run` (#18236)

Our `run` function which executes top level awaits (and synchronous
statements in-between/after) did not unset the context in time in case
the function returns an async value. In that case the context was still
around until the that promise resolves, which can be too late because
unrelated things can be intertwined with the batch.

The test shows this: Without the fix, the unrelated count incrementation
would not update the view until the top level awaits in the child are
done. In the test this just shows as a delayed visual update, but it
also can result in stale roots as shown in
https://github.com/sveltejs/svelte/issues/18221#issuecomment-4470921077
pull/18239/head
Simon H 3 weeks ago committed by GitHub
parent 4d8f99a270
commit eb10a70cf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: unset context synchronously in `run`

@ -303,15 +303,21 @@ export function run(thunks) {
.then(() => {
restore();
if (errored) {
throw errored.error;
try {
if (errored) {
throw errored.error;
}
if (aborted(active)) {
throw STALE_REACTION;
}
return fn();
} finally {
// We gotta unset context directly in case the function returns a promise, in which case
// unset_context in .finally() would be too late ...
unset_context();
}
if (aborted(active)) {
throw STALE_REACTION;
}
return fn();
})
.catch(handle_error);
@ -320,6 +326,7 @@ export function run(thunks) {
promise.finally(() => {
blocker.settled = true;
// ... but we also need it after such a promise has resolved in case it restores our context
unset_context();
});
}

@ -0,0 +1,7 @@
<script>
import { push } from "./main.svelte";
const x = await push(1);
const y = await push(2);
</script>
{x} {y}

@ -0,0 +1,26 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [show, resolve, count] = target.querySelectorAll('button');
show.click();
await tick();
resolve.click();
await tick();
count.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>show</button> <button>resolve</button> <button>1</button>'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'1 2 <button>show</button> <button>resolve</button> <button>1</button>'
);
}
});

@ -0,0 +1,23 @@
<script module>
const queued = [];
export function push(v) {
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<script>
import Child from "./Child.svelte";
let show = $state(false);
let count = $state(0);
</script>
{#if show}
<Child />
{/if}
<button onclick={() => show = true}>show</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<button onclick={() => count++}>{count}</button>
Loading…
Cancel
Save