diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 97da435d0a..b78aa6880c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -7,9 +7,27 @@ */ export function AwaitExpression(node, context) { const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; - const blocking = tla || !!context.state.expression; + let suspend = tla; - if (blocking) { + if (context.state.expression) { + // wrap the expression in `(await $.suspend(...)).exit()` if necessary, + // i.e. whether anything could potentially be read _after_ the await + let i = context.path.length; + while (i--) { + const parent = context.path[i]; + + // @ts-expect-error we could probably use a neater/more robust mechanism + if (parent.metadata?.expression === context.state.expression) { + break; + } + + // TODO make this more accurate — we don't need to call suspend + // if this is the last thing that could be read + suspend = true; + } + } + + if (suspend) { if (!context.state.analysis.runes) { throw new Error('TODO runes mode only'); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index a33b07d2b9..51c6f428d4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -53,7 +53,7 @@ export interface ComponentClientTransformState extends ClientTransformState { /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; /** Expressions used inside the render effect */ - readonly expressions: Expression[]; + readonly expressions: Array<{ id: Identifier; expression: Expression; is_async: boolean }>; /** The HTML template string */ readonly template: Array; readonly locations: SourceLocation[]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 21a78de032..32ff9d530e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -364,7 +364,11 @@ export function RegularElement(node, context) { // (e.g. `{location}`), set `textContent` programmatically const use_text_content = trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') && - trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) && + trimmed.every( + (node) => + node.type === 'Text' || + (!node.metadata.expression.has_state && !node.metadata.expression.is_async) + ) && trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { @@ -537,8 +541,8 @@ function build_element_attribute_update_assignment( const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; - let { value, has_state } = build_attribute_value(attribute.value, context, (value) => - get_expression_id(state, value) + let { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => + get_expression_id(state, value, is_async) ); if (name === 'autofocus') { @@ -665,8 +669,8 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co */ function build_element_special_value_attribute(element, node_id, attribute, context) { const state = context.state; - const { value, has_state } = build_attribute_value(attribute.value, context, (value) => - get_expression_id(state, value) + const { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => + get_expression_id(state, value, is_async) ); const inner_assignment = b.assignment( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 8fb6b8bdde..2e126004ae 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -35,8 +35,10 @@ export function build_set_attributes( for (const attribute of attributes) { if (attribute.type === 'Attribute') { - const { value, has_state } = build_attribute_value(attribute.value, context, (value) => - get_expression_id(context.state, value) + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, is_async) => get_expression_id(context.state, value, is_async) ); if ( @@ -111,8 +113,8 @@ export function build_style_directives( let value = directive.value === true ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value) => - get_expression_id(context.state, value) + : build_attribute_value(directive.value, context, (value, is_async) => + get_expression_id(context.state, value, is_async) ).value; const update = b.stmt( @@ -149,11 +151,11 @@ export function build_class_directives( ) { const state = context.state; for (const directive of class_directives) { - const { has_state, has_call } = directive.metadata.expression; + const { has_state, has_call, is_async } = directive.metadata.expression; let value = /** @type {Expression} */ (context.visit(directive.expression)); - if (has_call) { - value = get_expression_id(state, value); + if (has_call || is_async) { + value = get_expression_id(state, value, is_async); } const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); @@ -169,7 +171,7 @@ export function build_class_directives( /** * @param {AST.Attribute['value']} value * @param {ComponentContext} context - * @param {(value: Expression) => Expression} memoize + * @param {(value: Expression, is_async: boolean) => Expression} memoize * @returns {{ value: Expression, has_state: boolean }} */ export function build_attribute_value(value, context, memoize = (value) => value) { @@ -187,7 +189,10 @@ export function build_attribute_value(value, context, memoize = (value) => value let expression = /** @type {Expression} */ (context.visit(chunk.expression)); return { - value: chunk.metadata.expression.has_call ? memoize(expression) : expression, + value: + chunk.metadata.expression.has_call || chunk.metadata.expression.is_async + ? memoize(expression, chunk.metadata.expression.is_async) + : expression, has_state: chunk.metadata.expression.has_state }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index c4f81274d9..ac33e9686c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -23,16 +23,20 @@ export function memoize_expression(state, value) { /** * * @param {ComponentClientTransformState} state - * @param {Expression} value + * @param {Expression} expression + * @param {boolean} is_async */ -export function get_expression_id(state, value) { +export function get_expression_id(state, expression, is_async) { for (let i = 0; i < state.expressions.length; i += 1) { - if (compare_expressions(state.expressions[i], value)) { - return b.id(`$${i}`); + if (compare_expressions(state.expressions[i].expression, expression)) { + return state.expressions[i].id; } } - return b.id(`$${state.expressions.push(value) - 1}`); + const id = b.id(''); // filled in later + state.expressions.push({ id, expression, is_async }); + + return id; } /** @@ -79,14 +83,14 @@ function compare_expressions(a, b) { * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @param {(value: Expression) => Expression} memoize - * @returns {{ value: Expression, has_state: boolean }} + * @param {(value: Expression, is_async: boolean) => Expression} memoize + * @returns {{ value: Expression, has_state: boolean, is_async: boolean }} */ export function build_template_chunk( values, visit, state, - memoize = (value) => get_expression_id(state, value) + memoize = (value, is_async) => get_expression_id(state, value, is_async) ) { /** @type {Expression[]} */ const expressions = []; @@ -95,6 +99,7 @@ export function build_template_chunk( const quasis = [quasi]; let has_state = false; + let is_async = false; for (let i = 0; i < values.length; i++) { const node = values[i]; @@ -108,16 +113,17 @@ export function build_template_chunk( } else { let value = /** @type {Expression} */ (visit(node.expression, state)); - has_state ||= node.metadata.expression.has_state; + is_async ||= node.metadata.expression.is_async; + has_state ||= is_async || node.metadata.expression.has_state; - if (node.metadata.expression.has_call) { - value = memoize(value); + if (node.metadata.expression.has_call || node.metadata.expression.is_async) { + value = memoize(value, node.metadata.expression.is_async); } if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value, has_state }; + return { value, has_state, is_async }; } else { let expression = value; // only add nullish coallescence if it hasn't been added already @@ -148,25 +154,34 @@ export function build_template_chunk( const value = b.template(quasis, expressions); - return { value, has_state }; + return { value, has_state, is_async }; } /** * @param {ComponentClientTransformState} state */ export function build_render_statement(state) { + const sync = state.expressions.filter(({ is_async }) => !is_async); + const async = state.expressions.filter(({ is_async }) => is_async); + + const all = [...sync, ...async]; + + for (let i = 0; i < all.length; i += 1) { + all[i].id.name = `$${i}`; + } + return b.stmt( b.call( '$.template_effect', b.arrow( - state.expressions.map((_, i) => b.id(`$${i}`)), + all.map(({ id }) => id), state.update.length === 1 && state.update[0].type === 'ExpressionStatement' ? state.update[0].expression : b.block(state.update) ), - state.expressions.length > 0 && - b.array(state.expressions.map((expression) => b.thunk(expression))), - state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal') + all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))), + async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))), + !state.analysis.runes && sync.length > 0 && b.id('$.derived_safe_equal') ) ); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index eb0fdba469..8638ed9ee6 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect } from '#client' */ +/** @import { Derived, Effect, Source } from '#client' */ import { DEV } from 'esm-env'; import { CLEAN, @@ -80,10 +80,10 @@ export function derived(fn) { /** * @template V * @param {() => Promise} fn - * @returns {Promise<() => V>} + * @returns {Promise>} */ /*#__NO_SIDE_EFFECTS__*/ -export async function async_derived(fn) { +export function async_derived(fn) { if (!active_effect) { throw new Error('TODO cannot create unowned async derived'); } @@ -103,10 +103,7 @@ export async function async_derived(fn) { // TODO what happens when the promise rejects? }); - // wait for the initial promise - (await suspend(promise)).exit(); - - return () => get(value); + return promise.then(() => value); } /** diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 1cd390d17a..cb09ca06ac 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ +/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager, Value } from '#client' */ import { check_dirtiness, component_context, @@ -44,7 +44,8 @@ import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; -import { derived, destroy_derived } from './deriveds.js'; +import { async_derived, derived, destroy_derived } from './deriveds.js'; +import { suspend } from '../dom/blocks/boundary.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -345,11 +346,18 @@ export function render_effect(fn) { /** * @param {(...expressions: any) => void | (() => void)} fn - * @param {Array<() => any>} thunks - * @returns {Effect} + * @param {Array<() => any>} sync + * @param {Array<() => Promise>} async */ -export function template_effect(fn, thunks = [], d = derived) { - const deriveds = thunks.map(d); +export async function template_effect(fn, sync = [], async = [], d = derived) { + /** @type {Value[]} */ + const deriveds = sync.map(d); + + if (async.length > 0) { + const async_deriveds = (await suspend(Promise.all(async.map(async_derived)))).exit(); + deriveds.push(...async_deriveds); + } + const effect = () => fn(...deriveds.map(get)); if (DEV) { @@ -358,7 +366,7 @@ export function template_effect(fn, thunks = [], d = derived) { }); } - return block(effect); + block(effect); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js index 5f85050d9b..26333c05fc 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -19,6 +19,8 @@ export default test({ async test({ assert, target }) { d.resolve('hello'); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); }