fix: block declaration tags on async values read inside closures (#18533)

Fixes #18469

Declaration tags collect blockers from `metadata.expression.dependencies`, which only sees eager reads, so a `$derived` reading an async declaration inside a closure gets no blocker and evaluates while the value is still `undefined`. Align with template expressions which already collect blockers from `references`

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/18722/head
Nic Polumeyv 4 days ago committed by GitHub
parent aadc97ce1b
commit 4d5139552d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: block declaration tags and `{@const}` on async values read inside closures

@ -43,7 +43,8 @@ export function DeclarationTag(node, context) {
*/ */
export function mark_async_declaration(context, metadata, declarations) { export function mark_async_declaration(context, metadata, declarations) {
const has_await = metadata.expression.has_await; const has_await = metadata.expression.has_await;
const blockers = [...metadata.expression.dependencies] // reads inside closures must block too, like they do in template expressions
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker) .map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id); .filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -71,7 +71,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name)); context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
} }
const blockers = [...metadata.expression.dependencies] const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker) .map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id); .filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -66,7 +66,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name)); context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
} }
const blockers = [...metadata.expression.dependencies] const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker) .map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id); .filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -177,11 +177,11 @@ export function runtime_suite(runes: boolean) {
['dom', 'hydrate', 'ssr', 'async-ssr'], ['dom', 'hydrate', 'ssr', 'async-ssr'],
(variant, config, test_name) => { (variant, config, test_name) => {
if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) { if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) {
return true; return 'no-test';
} }
if (async_mode && config.skip_async) { if (async_mode && config.skip_async) {
return true; return 'no-test';
} }
if (variant === 'hydrate') { if (variant === 'hydrate') {
@ -195,9 +195,9 @@ export function runtime_suite(runes: boolean) {
) { ) {
return 'no-test'; return 'no-test';
} }
if (variant === 'ssr') { if (variant === 'ssr') {
if ( if (
(test_name.startsWith('async-') && !config.mode?.includes('server')) ||
(config.mode && !config.mode.includes('server')) || (config.mode && !config.mode.includes('server')) ||
(!config.test_ssr && (!config.test_ssr &&
config.html === undefined && config.html === undefined &&

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
// #18469 — a @const in a nested snippet reading an async declaration through a closure must block on it
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>true</p> <p>false</p>');
}
});

@ -0,0 +1,12 @@
<script>
async function getValue() {
return new Set(['a', 'b', 'c']);
}
const value = await getValue();
</script>
{#each [['a', 'b'], ['a', 'x']] as keys}
{@const all_present = keys.every((k) => value.has(k))}
<p>{all_present}</p>
{/each}

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
// #18469 — a sync $derived in a nested snippet reading an async declaration through a closure must block on it
export default test({
ssrHtml: '<p>true</p> <p>false</p>',
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>true</p> <p>false</p>');
}
});

@ -0,0 +1,17 @@
<script>
async function getValue() {
return new Set(['a', 'b', 'c']);
}
</script>
{#snippet outer()}
{const value = $derived(await getValue())}
{#snippet inner(keys)}
{const all_present = $derived(keys.every((k) => value.has(k)))}
<p>{all_present}</p>
{/snippet}
{@render inner(['a', 'b'])}
{@render inner(['a', 'x'])}
{/snippet}
{@render outer()}
Loading…
Cancel
Save