From 69a1902a22ad7b9bed5a37885ebd5fd3403b8401 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 18:50:36 -0500 Subject: [PATCH 1/4] small fix --- packages/svelte/src/internal/client/dom/blocks/async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index c3073d8611..19527283a1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -14,7 +14,7 @@ export function async(node, expressions, fn) { var restore = capture(); var unsuspend = suspend(); - Promise.all(expressions.map(async_derived)).then((result) => { + Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { restore(); fn(node, ...result); unsuspend(); From 461c081cd123018b6effc3607b34757c108e5c01 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 21:47:12 -0500 Subject: [PATCH 2/4] error handling --- .../internal/client/dom/blocks/boundary.js | 18 ++++++--- .../internal/client/reactivity/deriveds.js | 4 +- .../samples/async-error/_config.js | 37 +++++++++++++++++++ .../samples/async-error/main.svelte | 16 ++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index eaffd07ce3..5c768be99b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -136,6 +136,12 @@ export function boundary(node, props, children) { } function reset() { + async_count = 0; + + if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { + boundary.f ^= BOUNDARY_SUSPENDED; + } + if (failed_effect !== null) { pause_effect(failed_effect, () => { failed_effect = null; @@ -151,6 +157,11 @@ export function boundary(node, props, children) { reset_is_throwing_error(); } }); + + if (async_count > 0) { + boundary.f |= BOUNDARY_SUSPENDED; + show_pending_snippet(true); + } } function unsuspend() { @@ -367,12 +378,7 @@ export function boundary(node, props, children) { }); }); } else { - main_effect = branch(() => children(anchor)); - - if (async_count > 0) { - boundary.f |= BOUNDARY_SUSPENDED; - show_pending_snippet(true); - } + reset(); } reset_is_throwing_error(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 3747840f0f..076ad8dc8f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -147,7 +147,9 @@ export function async_derived(fn, detect_waterfall = true) { } }, (e) => { - handle_error(e, parent, null, parent.ctx); + if (promise === current) { + handle_error(e, parent, null, parent.ctx); + } } ); }, EFFECT_PRESERVED); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js new file mode 100644 index 0000000000..9c7e296287 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -0,0 +1,37 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.reject(new Error('oops!')); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

oops!

'); + + const button = target.querySelector('button'); + + component.promise = (d = deferred()).promise; + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('wheee'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte new file mode 100644 index 0000000000..dd42fa7596 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -0,0 +1,16 @@ + + + +

{await promise}

+ + {#snippet pending()} +

pending

+ {/snippet} + + {#snippet failed(error, reset)} +

{error.message}

+ + {/snippet} +
From 0b9bfc9a31c5033f01b8e93b8470376a442fd984 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 6 Feb 2025 07:26:34 -0500 Subject: [PATCH 3/4] async derived cannot use $derived.by --- .../client/visitors/VariableDeclaration.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index bba554c12a..e7ad5fe1e4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -167,17 +167,7 @@ export function VariableDeclaration(node, context) { declarations.push( b.declarator( declarator.id, - b.call( - b.await( - b.call( - '$.save', - b.call( - '$.async_derived', - rune === '$derived.by' ? value : b.thunk(value, true) - ) - ) - ) - ) + b.call(b.await(b.call('$.save', b.call('$.async_derived', b.thunk(value, true))))) ) ); } else { From 3289ac3ad159b194c95c3f5a397e397a79491682 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 6 Feb 2025 07:37:24 -0500 Subject: [PATCH 4/4] slightly better waterfall warning --- .../.generated/client-warnings.md | 2 +- .../messages/client-warnings/warnings.md | 2 +- .../client/visitors/VariableDeclaration.js | 19 ++++++++++++++++--- packages/svelte/src/constants.js | 1 + .../internal/client/reactivity/deriveds.js | 8 ++++---- .../src/internal/client/reactivity/effects.js | 2 +- .../svelte/src/internal/client/warnings.js | 7 ++++--- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index ba5f957f8d..82add74353 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -45,7 +45,7 @@ TODO ### await_waterfall ``` -Detected an unnecessary async waterfall +An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app. ``` TODO diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index eba1454bf7..4108cd2fcb 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -38,7 +38,7 @@ TODO ## await_waterfall -> Detected an unnecessary async waterfall +> An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app. TODO diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index e7ad5fe1e4..f047fddbdf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -1,7 +1,7 @@ /** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ -import { dev } from '../../../../state.js'; +import { dev, is_ignored, locate_node } from '../../../../state.js'; import { extract_paths } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; @@ -19,7 +19,7 @@ export function VariableDeclaration(node, context) { if (context.state.analysis.runes) { for (const declarator of node.declarations) { - const init = declarator.init; + const init = /** @type {Expression} */ (declarator.init); const rune = get_rune(init, context.state.scope); if ( @@ -164,10 +164,23 @@ export function VariableDeclaration(node, context) { if (declarator.id.type === 'Identifier') { if (is_async) { + const location = dev && is_ignored(init, 'await_waterfall') && locate_node(init); + declarations.push( b.declarator( declarator.id, - b.call(b.await(b.call('$.save', b.call('$.async_derived', b.thunk(value, true))))) + b.call( + b.await( + b.call( + '$.save', + b.call( + '$.async_derived', + b.thunk(value, true), + location ? b.literal(location) : undefined + ) + ) + ) + ) ) ); } else { diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 03fddc5ebd..d49d70536b 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -39,6 +39,7 @@ export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML'; // we use a list of ignorable runtime warnings because not every runtime warning // can be ignored and we want to keep the validation for svelte-ignore in place export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([ + 'await_waterfall', 'state_snapshot_uncloneable', 'binding_property_non_reactive', 'hydration_attribute_changed', diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 076ad8dc8f..c2da6639b8 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -88,11 +88,11 @@ export function derived(fn) { /** * @template V * @param {() => Promise} fn - * @param {boolean} detect_waterfall Whether to print a warning if the value is not read immediately after update + * @param {string} [location] If provided, print a warning if the value is not read immediately after update * @returns {Promise>} */ /*#__NO_SIDE_EFFECTS__*/ -export function async_derived(fn, detect_waterfall = true) { +export function async_derived(fn, location) { let parent = /** @type {Effect | null} */ (active_effect); if (parent === null) { @@ -129,12 +129,12 @@ export function async_derived(fn, detect_waterfall = true) { internal_set(signal, v); - if (DEV && detect_waterfall) { + if (DEV && location !== undefined) { recent_async_deriveds.add(signal); setTimeout(() => { if (recent_async_deriveds.has(signal)) { - w.await_waterfall(); + w.await_waterfall(location); recent_async_deriveds.delete(signal); } }); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2ab2908c77..0691b86180 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -352,7 +352,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var restore = capture(); var unsuspend = suspend(); - Promise.all(async.map((expression) => async_derived(expression, false))).then((result) => { + Promise.all(async.map((expression) => async_derived(expression))).then((result) => { restore(); if ((effect.f & DESTROYED) !== 0) { diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index 79fbebee4c..15196d3654 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -30,11 +30,12 @@ export function await_reactivity_loss() { } /** - * Detected an unnecessary async waterfall + * An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app. + * @param {string} location */ -export function await_waterfall() { +export function await_waterfall(location) { if (DEV) { - console.warn(`%c[svelte] await_waterfall\n%cDetected an unnecessary async waterfall\nhttps://svelte.dev/e/await_waterfall`, bold, normal); + console.warn(`%c[svelte] await_waterfall\n%cAn async value (${location}) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app.\nhttps://svelte.dev/e/await_waterfall`, bold, normal); } else { console.warn(`https://svelte.dev/e/await_waterfall`); }