fix: abort running obsolete async branches (#18118)

We shouldn't continue executing async work where we know the surrounding
branch is destroyed already, it can leave to noisy "derived inter"
warnings or even runtime errors ("cannot stringify symbol" when running
a template effect with an uninitialized source). Neither should we warn
about waterfalls on an already-destroyed async effect.

Fixes #18097 (though strictly speaking that particular instance is also
fixed by #18117 which fixes the underlying cause for the reruns; this
one is necessary in itself though, as shown by the new test)

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18184/head
Simon H 2 months ago committed by GitHub
parent 91e1ead773
commit 1c150a460f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: abort running obsolete async branches

@ -55,14 +55,16 @@ export function flatten(blockers, sync, async, fn) {
/** @param {Value[]} values */
function finish(values) {
if ((parent.f & DESTROYED) !== 0) {
return;
}
restore();
try {
fn(values);
} catch (error) {
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
invoke_error_boundary(error, parent);
}
unset_context();

@ -239,7 +239,7 @@ export function async_derived(fn, label, location) {
recent_async_deriveds.add(signal);
setTimeout(() => {
if (recent_async_deriveds.has(signal)) {
if (recent_async_deriveds.has(signal) && (effect.f & DESTROYED) === 0) {
w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal);
}

@ -0,0 +1,6 @@
<script>
let { count } = $props();
let double = $derived(count * 2);
$effect.pre(() => console.log(count, double));
</script>

@ -0,0 +1,27 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs, warnings }) {
const [increment, resolve] = target.querySelectorAll('button');
increment.click();
await tick();
assert.deepEqual(logs, []);
resolve.click();
await tick();
assert.deepEqual(logs, []);
resolve.click();
await tick();
assert.deepEqual(logs, []);
resolve.click();
await tick();
assert.deepEqual(logs, [1, 2]);
// no await waterfall / inert derived warnings
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,31 @@
<script>
import Child from "./Child.svelte";
let count = $state(0);
let deferreds = [];
function push(v) {
return new Promise((resolve, reject) => {
deferreds.push({ resolve: () => resolve(v), reject });
});
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => deferreds.shift()?.resolve()}>resolve</button>
<svelte:boundary>
{#if count % 2 === 0}
{@const double = count * 2}
<p>true</p>
{await push(count)} {double}
<Child count={await push(count)} />
{:else}
<p>false</p>
<Child count={await push(count)} />
{/if}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save