fix: abort running obsolete async branches

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)
async-obsolete-branch-fix
Simon Holthausen 3 weeks ago
parent 7fddfbdbbd
commit a2bbfb9173

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

@ -49,23 +49,29 @@ export function async(node, blockers = [], expressions = [], fn) {
set_hydrate_node(end);
}
flatten(blockers, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
fn(node, ...values);
} finally {
flatten(
blockers,
[],
expressions,
(values) => {
if (was_hydrating) {
set_hydrating(false);
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
decrement_pending();
}
});
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
fn(node, ...values);
} finally {
if (was_hydrating) {
set_hydrating(false);
}
decrement_pending();
}
},
() => decrement_pending()
);
}

@ -31,8 +31,9 @@ import { aborted } from './effects.js';
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
* @param {() => void} [on_abort]
*/
export function flatten(blockers, sync, async, fn) {
export function flatten(blockers, sync, async, fn, on_abort) {
const d = is_runes() ? derived : derived_safe_equal;
// Filter out already-settled blockers - no need to wait for them
@ -57,12 +58,14 @@ export function flatten(blockers, sync, async, fn) {
function finish(values) {
restore();
try {
fn(values);
} catch (error) {
if ((parent.f & DESTROYED) === 0) {
if ((parent.f & DESTROYED) === 0) {
try {
fn(values);
} catch (error) {
invoke_error_boundary(error, parent);
}
} else {
on_abort?.();
}
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);
}

@ -400,13 +400,18 @@ export function deferred_template_effect(fn, sync = [], async = [], blockers = [
var decrement_pending = increment_pending();
}
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)));
if (decrement_pending) {
decrement_pending();
flatten(
blockers,
sync,
async,
(values) => {
create_effect(EFFECT, () => fn(...values.map(get)));
decrement_pending?.();
},
() => {
decrement_pending?.();
}
});
);
}
/**

@ -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