diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index fa10cc6ec0..d747d7b81a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -555,8 +555,7 @@ export function analyze_component(root, source, options) { snippets: new Set(), async_deriveds: new Set(), pickled_awaits: new Set(), - awaited_statements: new Map(), - promise_indexes: new Map() + awaited_statements: new Map() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 007f63bbb2..470e8035f5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -26,12 +26,12 @@ export function IfBlock(node, context) { } // TODO helperise - const promise_index = Array.from(node.metadata.expression.dependencies).reduce( - (index, binding) => Math.max(index, context.state.analysis.promise_indexes.get(binding) ?? -1), - -1 - ); + const blockers = new Set(); + for (const d of node.metadata.expression.dependencies) { + if (d.blocker) blockers.add(d.blocker); + } - const is_async = promise_index > -1 || node.metadata.expression.has_await; + const is_async = blockers.size > 0 || node.metadata.expression.has_await; const expression = build_expression(context, node.test, node.metadata.expression); const test = is_async ? b.call('$.get', b.id('$$condition')) : expression; @@ -84,7 +84,7 @@ export function IfBlock(node, context) { b.call( '$.async', context.state.node, - promise_index === -1 ? undefined : b.id(`$$promises[${promise_index}]`), + b.array([...blockers]), b.array([b.thunk(expression, node.metadata.expression.has_await)]), b.arrow([context.state.node, b.id('$$condition')], b.block(statements)) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 6db73808e4..c124ddc03c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -166,7 +166,7 @@ function transform_body(program, context) { /** @type {AwaitedStatement[]} */ const deriveds = []; - const { awaited_statements, promise_indexes } = context.state.analysis; + const { awaited_statements } = context.state.analysis; let awaited = false; @@ -303,19 +303,27 @@ function transform_body(program, context) { return b.thunk(b.block([/** @type {Statement} */ (s.node)]), s.has_await); }); - out.push(b.var('$$promises', b.call('$.run', b.array(thunks)))); - } + var id = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - // console.log('statements', statements); - // console.log('deriveds', deriveds); + out.push(b.var(id, b.call('$.run', b.array(thunks)))); - for (let i = 0; i < statements.length; i += 1) { - const s = statements[i]; + for (let i = 0; i < statements.length; i += 1) { + const s = statements[i]; - for (const binding of s.declarations) { - promise_indexes.set(binding, i); + var blocker = b.member(id, b.literal(i), true); + + for (const binding of s.declarations) { + binding.blocker = blocker; + } } + + // TODO we likely need to account for updates that happen after the declaration, + // e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise + // a synchronous `{obj.foo}` will fail } + // console.log('statements', statements); + // console.log('deriveds', deriveds); + return out; } 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 85841e13b8..49f4607b18 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 @@ -271,10 +271,7 @@ export function RegularElement(node, context) { const { value, has_state } = build_attribute_value( attribute.value, context, - (value, metadata) => - metadata.has_call || metadata.has_await - ? context.state.memoizer.add(value, metadata) - : value + (value, metadata) => context.state.memoizer.add(value, metadata) ); const update = build_element_attribute_update(node, node_id, name, value, attributes); @@ -518,8 +515,6 @@ export function build_class_directives_object( memoizer = context.state.memoizer ) { let properties = []; - let has_call_or_state = false; - let has_await = false; const metadata = create_expression_metadata(); @@ -528,13 +523,11 @@ export function build_class_directives_object( const expression = /** @type Expression */ (context.visit(d.expression)); properties.push(b.init(d.name, expression)); - has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_await ||= d.metadata.expression.has_await; } const directives = b.object(properties); - return has_call_or_state || has_await ? memoizer.add(directives, metadata) : directives; + return memoizer.add(directives, metadata); } /** @@ -551,9 +544,6 @@ export function build_style_directives_object( const normal = b.object([]); const important = b.object([]); - let has_call_or_state = false; - let has_await = false; - const metadata = create_expression_metadata(); for (const d of style_directives) { @@ -566,14 +556,11 @@ export function build_style_directives_object( const object = d.modifiers.includes('important') ? important : normal; object.properties.push(b.init(d.name, expression)); - - has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_await ||= d.metadata.expression.has_await; } const directives = important.properties.length ? b.array([normal, important]) : normal; - return has_call_or_state || has_await ? memoizer.add(directives, metadata) : directives; + returnmemoizer.add(directives, metadata); } /** @@ -702,7 +689,7 @@ function build_element_special_value_attribute( element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata) : value + state.memoizer.add(value, metadata) ); const evaluated = context.state.scope.evaluate(value); 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 29ca440d66..4fd255ca26 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,7 +35,7 @@ export function build_attribute_effect( for (const attribute of attributes) { if (attribute.type === 'Attribute') { const { value } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await ? memoizer.add(value, metadata) : value + memoizer.add(value, metadata) ); if ( @@ -52,9 +52,7 @@ export function build_attribute_effect( } else { let value = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) { - value = memoizer.add(value, attribute.metadata.expression); - } + value = memoizer.add(value, attribute.metadata.expression); values.push(b.spread(value)); } @@ -155,9 +153,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c value = b.call('$.clsx', value); } - return metadata.has_call || metadata.has_await - ? context.state.memoizer.add(value, metadata) - : value; + return context.state.memoizer.add(value, metadata); }); /** @type {Identifier | undefined} */ @@ -227,7 +223,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c */ export function build_set_style(node_id, attribute, style_directives, context) { let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? context.state.memoizer.add(value, metadata) : value + context.state.memoizer.add(value, metadata) ); /** @type {Identifier | undefined} */ 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 45a421ccca..282e2c88ac 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 @@ -21,11 +21,25 @@ export class Memoizer { /** @type {Array<{ id: Identifier, expression: Expression }>} */ #async = []; + /** @type {Set} */ + #blockers = new Set(); + /** * @param {Expression} expression * @param {ExpressionMetadata} metadata */ add(expression, metadata) { + for (const binding of metadata.dependencies) { + if (binding.blocker) { + this.#blockers.add(binding.blocker); + } + } + + if (!metadata.has_call && !metadata.has_await) { + // no memoization required + return expression; + } + const id = b.id('#'); // filled in later (metadata.has_await ? this.#async : this.#sync).push({ id, expression }); @@ -40,6 +54,10 @@ export class Memoizer { }); } + blockers() { + return b.array([...this.#blockers]); + } + deriveds(runes = true) { return this.#sync.map((memo) => b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression))) @@ -72,8 +90,7 @@ export function build_template_chunk( values, context, state = context.state, - memoize = (value, metadata) => - metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata) : value + memoize = (value, metadata) => state.memoizer.add(value, metadata) ) { /** @type {Expression[]} */ const expressions = []; @@ -169,6 +186,7 @@ export function build_render_statement(state) { return b.stmt( b.call( '$.template_effect', + memoizer.blockers(), b.arrow( ids, state.update.length === 1 && state.update[0].type === 'ExpressionStatement' diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f7d3dac0f7..e4a7b22be8 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -129,6 +129,15 @@ export class Binding { mutated = false; reassigned = false; + /** + * Instance-level declarations may follow (or contain) a top-level `await`. In these cases, + * any reads that occur in the template must wait for the corresponding promise to resolve + * otherwise the initial value will not have been assigned + * TODO the blocker is set during transform which feels a bit grubby + * @type {Expression | null} + */ + blocker = null; + /** * * @param {Scope} scope diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index efec50cafb..43daa19f09 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -134,10 +134,4 @@ export interface ComponentAnalysis extends Analysis { * so that we can run the template synchronously */ awaited_statements: Map; - /** - * A map that tells us which of the `$$promises` needs to be awaited - * before a particular binding can be accessed - * TODO this gets populated during transform, which feels wrong - */ - promise_indexes: Map; } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 1b4fe57aba..e8412e6cab 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -14,11 +14,11 @@ import { get_boundary } from './boundary.js'; /** * @param {TemplateNode} node - * @param {Promise | undefined} blocker + * @param {Array>} blockers * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export function async(node, blocker, expressions, fn) { +export function async(node, blockers, expressions, fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var blocking = !boundary.is_pending(); @@ -36,7 +36,7 @@ export function async(node, blocker, expressions, fn) { set_hydrate_node(end); } - flatten(blocker, [], expressions, (values) => { + flatten(blockers, [], expressions, (values) => { if (was_hydrating) { set_hydrating(true); set_hydrate_node(previous_hydrate_node); diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c62c0f65fe..70d511cc63 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -33,19 +33,18 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; -import { create_text } from '../dom/operations.js'; import { noop } from '../../shared/utils.js'; /** - * @param {Promise | undefined} blocker + * @param {Array>} blockers * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {(values: Value[]) => any} fn */ -export function flatten(blocker, sync, async, fn) { +export function flatten(blockers, sync, async, fn) { const d = is_runes() ? derived : derived_safe_equal; - if (async.length === 0) { + if (async.length === 0 && blockers.length === 0) { fn(sync.map(d)); return; } @@ -57,7 +56,7 @@ export function flatten(blocker, sync, async, fn) { var was_hydrating = hydrating; - Promise.resolve(blocker).then((values) => { + Promise.all(blockers).then(() => { restore(); const result = Promise.all(async.map((expression) => async_derived(expression))) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b338b34421..e50d7aa689 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -362,13 +362,14 @@ export function render_effect(fn, flags = 0) { } /** + * @param {Array>} blockers * @param {(...expressions: any) => void | (() => void)} fn * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {Promise} [blocker] */ -export function template_effect(fn, sync = [], async = [], blocker) { - flatten(blocker, sync, async, (values) => { +export function template_effect(blockers, fn, sync = [], async = [], blocker) { + flatten(blockers, sync, async, (values) => { create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); }); }