diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js index dcdae3587f..8471b4d99b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -22,6 +22,25 @@ export function IfBlock(node, context) { expression: node.metadata.expression }); + // TODO this can be helperised + for (const binding of node.metadata.expression.dependencies) { + const awaited = context.state.analysis.awaited_declarations.get(binding.node.name); + + if (awaited) { + node.metadata.async ??= { + declarations: new Set() + }; + + node.metadata.async.declarations.add(awaited); + } + } + + if (node.metadata.expression.has_await) { + node.metadata.async ??= { + declarations: new Set() + }; + } + context.visit(node.consequent); if (node.alternate) context.visit(node.alternate); } 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 3dd36b180b..f96879029c 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 @@ -25,9 +25,8 @@ export function IfBlock(node, context) { statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate))); } - const { has_await } = node.metadata.expression; const expression = build_expression(context, node.test, node.metadata.expression); - const test = has_await ? b.call('$.get', b.id('$$condition')) : expression; + const test = node.metadata.async ? b.call('$.get', b.id('$$condition')) : expression; /** @type {Expression[]} */ const args = [ @@ -71,13 +70,20 @@ export function IfBlock(node, context) { statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if')); - if (has_await) { + if (node.metadata.async) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - b.array([b.thunk(expression, true)]), + b.array([...node.metadata.async.declarations].map((d) => d.id)), + b.array([ + b.arrow( + [...node.metadata.async.declarations].map((d) => d.pattern), + 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 c834f83706..5b0e8322fd 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 @@ -1,4 +1,4 @@ -/** @import { Expression, Identifier, ImportDeclaration, MemberExpression, Program, Statement } from 'estree' */ +/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclarator } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { build_getter, is_prop_source } from '../utils.js'; import * as b from '#compiler/builders'; @@ -160,6 +160,50 @@ function transform_body(program, context) { /** @type {Identifier | null} */ let last = null; + /** + * @param {Statement | VariableDeclarator | FunctionDeclaration | ClassDeclaration} node + */ + const push = (node) => { + const awaited = awaited_statements.get(node); + + if (awaited) { + const ids = new Set(); + const patterns = new Set(); + + for (const binding of awaited.metadata.dependencies) { + const dep = awaited_declarations.get(binding.node.name); + if (dep && dep.id !== awaited.id && !ids.has(dep.id)) { + ids.add(dep.id); + patterns.add(dep.pattern); + } + } + + if (last) { + ids.add(last); + } + + const rhs = + node.type === 'VariableDeclarator' + ? node.init ?? b.block([]) + : node.type === 'ExpressionStatement' + ? node.expression + : b.block([node]); + + out.push( + b.var( + awaited.id, + b.call('$.run', b.array([...ids]), b.arrow([...patterns], rhs, awaited.has_await)) + ) + ); + + last = awaited.id; + } else if (node.type === 'VariableDeclarator') { + out.push(b.var(node.id, node.init)); + } else { + out.push(node); + } + }; + for (let node of program.body) { if (node.type === 'ImportDeclaration') { // TODO we can get rid of the visitor @@ -183,67 +227,13 @@ function transform_body(program, context) { if (node.type === 'VariableDeclaration') { for (const declarator of node.declarations) { - const awaited = awaited_statements.get(declarator); - - if (awaited) { - // TODO dependencies - out.push( - b.var( - awaited.id, - b.call( - '$.run', - b.array([]), - b.arrow([], declarator.init ?? b.block([]), awaited.has_await) - ) - ) - ); - - last = awaited.id; - } else { - out.push(b.var(declarator.id, declarator.init)); - } + push(declarator); } } else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') { // TODO + push(node); } else { - const awaited = awaited_statements.get(node); - - if (awaited) { - const ids = new Set(); - const patterns = new Set(); - - for (const binding of awaited.metadata.dependencies) { - const dep = awaited_declarations.get(binding.node.name); - if (dep && !ids.has(dep.id)) { - ids.add(dep.id); - patterns.add(dep.pattern); - } - } - - if (last) { - ids.add(last); - } - - // TODO dependencies - out.push( - b.var( - awaited.id, - b.call( - '$.run', - b.array([...ids]), - b.arrow( - [...patterns], - node.type === 'ExpressionStatement' ? node.expression : b.block([node]), - awaited.has_await - ) - ) - ) - ); - - last = awaited.id; - } else { - out.push(node); - } + push(node); } } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index baaa64f0e3..e62c2da735 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -31,6 +31,20 @@ export interface ReactiveStatement { dependencies: Binding[]; } +export interface AwaitedDeclaration { + id: Identifier; + has_await: boolean; + pattern: Pattern; + metadata: ExpressionMetadata; + updated_by: Set; +} + +export interface AwaitedStatement { + id: Identifier; + has_await: boolean; + metadata: ExpressionMetadata; +} + /** * Analysis common to modules and components */ @@ -125,22 +139,10 @@ export interface ComponentAnalysis extends Analysis { * * ...and references to `a` or `b` in the template should be mediated by `$$0` and `$$1` */ - awaited_declarations: Map< - string, - { - id: Identifier; - has_await: boolean; - pattern: Pattern; - metadata: ExpressionMetadata; - updated_by: Set; - } - >; + awaited_declarations: Map; /** * Information about top-level instance statements that need to be transformed * so that we can run the template synchronously */ - awaited_statements: Map< - Statement | ModuleDeclaration | VariableDeclarator, - { id: Identifier; has_await: boolean; metadata: ExpressionMetadata } - >; + awaited_statements: Map; } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f38706d075..e823b57799 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -17,6 +17,7 @@ import type { } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css'; +import type { AwaitedDeclaration } from '../phases/types'; /** * - `html` — the default, for e.g. `
` or `` @@ -465,6 +466,9 @@ export namespace AST { alternate: Fragment | null; /** @internal */ metadata: { + async: { + declarations: Set; + }; expression: ExpressionMetadata; }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 5ee9d25bce..107649be25 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -14,10 +14,11 @@ import { get_boundary } from './boundary.js'; /** * @param {TemplateNode} node + * @param {Array>} dependencies * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export function async(node, expressions, fn) { +export function async(node, dependencies, expressions, fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var blocking = !boundary.is_pending(); @@ -35,7 +36,7 @@ export function async(node, expressions, fn) { set_hydrate_node(end); } - flatten([], expressions, (values) => { + flatten(dependencies, [], expressions, (values) => { if (was_hydrating) { set_hydrating(true); set_hydrate_node(previous_hydrate_node); diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 471eed299d..9b5bc54740 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -100,6 +100,7 @@ export { export { async_body, for_await_track_reactivity_loss, + run, save, track_reactivity_loss } from './reactivity/async.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fb836df989..9540ed7993 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -36,12 +36,12 @@ import { import { create_text } from '../dom/operations.js'; /** - * + * @param {Array>} dependencies * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {(values: Value[]) => any} fn */ -export function flatten(sync, async, fn) { +export function flatten(dependencies, sync, async, fn) { const d = is_runes() ? derived : derived_safe_equal; if (async.length === 0) { @@ -56,29 +56,39 @@ export function flatten(sync, async, fn) { var was_hydrating = hydrating; - Promise.all(async.map((expression) => async_derived(expression))) - .then((result) => { - restore(); + Promise.all(dependencies).then((values) => { + restore(); - try { - fn([...sync.map(d), ...result]); - } catch (error) { - // ignore errors in blocks that have already been destroyed - if ((parent.f & DESTROYED) === 0) { - invoke_error_boundary(error, parent); + const result = Promise.all( + async.map((expression) => async_derived(() => expression(...values))) + ) + .then((result) => { + restore(); + + try { + fn([...sync.map(d), ...result]); + } catch (error) { + // ignore errors in blocks that have already been destroyed + if ((parent.f & DESTROYED) === 0) { + invoke_error_boundary(error, parent); + } } - } - if (was_hydrating) { - set_hydrating(false); - } + if (was_hydrating) { + set_hydrating(false); + } + + batch?.deactivate(); + unset_context(); + }) + .catch((error) => { + invoke_error_boundary(error, parent); + }); - batch?.deactivate(); - unset_context(); - }) - .catch((error) => { - invoke_error_boundary(error, parent); - }); + unset_context(); + + return result; + }); } /** @@ -259,3 +269,12 @@ export async function async_body(anchor, fn) { unset_context(); } } + +/** + * @param {Array>} deps + * @param {(...deps: any) => any} fn + */ +export function run(deps, fn) { + // TODO save/restore context + return Promise.all(deps).then(fn); +}