Rich Harris 6 days ago committed by GitHub
commit 378e924744
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,12 +1,17 @@
/** @import * as ESTree from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
/** @import { Analysis, AwaitedStatement, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers, has_await_expression } from '../../utils/ast.js';
import {
extract_identifiers,
has_await_expression,
object,
unwrap_pattern
} from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@ -543,7 +548,8 @@ export function analyze_component(root, source, options) {
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
pickled_awaits: new Set()
pickled_awaits: new Set(),
awaited_statements: new Map()
};
if (!runes) {
@ -687,6 +693,179 @@ export function analyze_component(root, source, options) {
e.legacy_rest_props_invalid(rest_props_refs[0].node);
}
if (instance.has_await) {
/**
* @param {ESTree.Expression} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Expression>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
/**
* @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.FunctionDeclaration | ESTree.ClassDeclaration} node
*/
const push = (node) => {
/** @type {Set<Binding>} */
const reads = new Set();
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
/** @type {AwaitedStatement} */
const statement = {
node,
has_await: has_await_expression(node),
declarations: [],
reads,
writes
};
analysis.awaited_statements.set(node, statement);
if (node.type === 'VariableDeclarator') {
for (const identifier of extract_identifiers(node.id)) {
const binding = /** @type {Binding} */ (instance.scope.get(identifier.name));
statement.declarations.push(binding);
}
} else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') {
const binding = /** @type {Binding} */ (instance.scope.get(node.id.name));
statement.declarations.push(binding);
}
};
for (let node of instance.ast.body) {
if (
node.type === 'ImportDeclaration' ||
node.type === 'ExportDefaultDeclaration' ||
node.type === 'ExportAllDeclaration'
) {
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
push(declarator);
}
} else {
push(node);
}
}
}
for (const { ast, scope, scopes } of [module, instance, template]) {
/** @type {AnalysisState} */
const state = {

@ -10,16 +10,13 @@ import * as e from '../../../errors.js';
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
tla ||
(is_reactive_expression(
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node))
!is_last_evaluated_expression(context.path, node)
) {
context.state.analysis.pickled_awaits.add(node);
}
@ -145,6 +142,9 @@ function is_last_evaluated_expression(path, node) {
if (node !== parent.expressions.at(-1)) return false;
break;
case 'VariableDeclarator':
return true;
default:
return false;
}

@ -370,41 +370,22 @@ export function client_component(analysis, options) {
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
if (analysis.instance.has_await) {
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports'));
}
const body = b.block([
...store_setup,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
...(should_inject_context && component_returned_object.length > 0
? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))]
: []),
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
component_block.body.push(
b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true)))
);
} else {
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
}
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))

@ -21,9 +21,6 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly transform: Record<
string,
{

@ -1,4 +1,4 @@
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';

@ -312,9 +312,10 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
const { has_await } = node.metadata.expression;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const is_async = node.metadata.expression.is_async();
const get_collection = b.thunk(collection, node.metadata.expression.has_await);
const thunk = is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -341,12 +342,13 @@ export function EachBlock(node, context) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([get_collection]),
b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
)

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';

@ -11,9 +11,10 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const html = is_async ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@ -30,13 +31,14 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
if (node.metadata.expression.has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
)
)

@ -25,9 +25,10 @@ export function IfBlock(node, context) {
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
}
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.test, node.metadata.expression);
const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
/** @type {Expression[]} */
const args = [
@ -71,13 +72,14 @@ export function IfBlock(node, context) {
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
)
)

@ -11,10 +11,10 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = build_expression(context, node.expression, node.metadata.expression);
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const key = b.thunk(is_async ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
let statement = add_svelte_meta(
@ -23,12 +23,13 @@ export function KeyBlock(node, context) {
'key'
);
if (has_await) {
if (is_async) {
statement = b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
)
);

@ -1,14 +1,16 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
import { runes } from '../../../../state.js';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} _
* @param {Program} node
* @param {ComponentContext} context
*/
export function Program(_, context) {
export function Program(node, context) {
if (!context.state.analysis.runes) {
context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +139,18 @@ export function Program(_, context) {
add_state_transformers(context);
if (context.state.is_instance && runes) {
return {
...node,
body: transform_body(
node,
context.state.analysis.awaited_statements,
b.id('$.run'),
(node) => /** @type {Node} */ (context.visit(node)),
(statement) => context.state.hoisted.push(statement)
)
};
}
context.next();
}

@ -23,10 +23,10 @@ export function RenderTag(node, context) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
let expression = build_expression(context, arg, metadata);
let expression = memoizer.add(build_expression(context, arg, metadata), metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata));
expression = b.call('$.get', expression);
}
args.push(b.thunk(expression));
@ -71,13 +71,15 @@ export function RenderTag(node, context) {
}
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
blockers,
memoizer.async_values(),
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)

@ -74,13 +74,15 @@ export function SlotElement(node, context) {
);
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
blockers,
async_values,
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)

@ -93,10 +93,10 @@ export function SvelteElement(node, context) {
);
}
const { has_await } = node.metadata.expression;
const is_async = node.metadata.expression.is_async();
const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@ -139,13 +139,14 @@ export function SvelteElement(node, context) {
)
);
if (has_await) {
if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
)
)

@ -193,18 +193,25 @@ export function VariableDeclaration(node, context) {
/** @type {CallExpression} */ (init)
);
// for now, only wrap async derived in $.save if it's not
// a top-level instance derived. TODO in future maybe we
// can dewaterfall all of them?
const should_save = context.state.is_instance && context.state.scope.function_depth > 1;
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
/** @type {Expression} */
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = save(call);
call = should_save ? save(call) : b.await(call);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
@ -224,18 +231,22 @@ export function VariableDeclaration(node, context) {
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
/** @type {Expression} */
let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression);
rhs = b.call('$.get', id);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = save(call);
call = should_save ? save(call) : b.await(call);
}
if (dev) {

@ -16,12 +16,12 @@ import { determine_slot } from '../../../../../utils/slot.js';
* @returns {Statement}
*/
export function build_component(node, component_name, context) {
/**
* @type {Expression}
*/
/** @type {Expression} */
const anchor = context.state.node;
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
@ -128,13 +128,16 @@ export function build_component(node, component_name, context) {
(events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
const expression = memoizer.add(
/** @type {Expression} */ (context.visit(attribute)),
attribute.metadata.expression
);
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression))
? b.call('$.get', expression)
: expression
)
);
@ -147,10 +150,10 @@ export function build_component(node, component_name, context) {
b.init(
attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) => {
const memoized = memoizer.add(value, metadata);
// TODO put the derived in the local block
return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata))
: value;
return metadata.has_call || metadata.has_await ? b.call('$.get', memoized) : memoized;
}).value
)
);
@ -184,9 +187,9 @@ export function build_component(node, component_name, context) {
);
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata, true))
: value;
const memoized = memoizer.add(value, metadata, should_wrap_in_derived);
return should_wrap_in_derived ? b.call('$.get', memoized) : memoized;
}
);
@ -497,12 +500,14 @@ export function build_component(node, component_name, context) {
memoizer.apply();
const async_values = memoizer.async_values();
const blockers = memoizer.blockers();
if (async_values) {
if (async_values || blockers) {
return b.stmt(
b.call(
'$.async',
anchor,
blockers,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
)

@ -89,6 +89,7 @@ export function build_attribute_effect(
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),

@ -22,12 +22,21 @@ export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#async = [];
/** @type {Set<Expression>} */
#blockers = new Set();
/**
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {boolean} memoize_if_state
*/
add(expression, metadata, memoize_if_state = false) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
const should_memoize =
metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state);
@ -50,6 +59,10 @@ export class Memoizer {
});
}
blockers() {
return this.#blockers.size > 0 ? b.array([...this.#blockers]) : undefined;
}
deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
@ -185,7 +198,8 @@ export function build_render_statement(state) {
: b.block(state.update)
),
memoizer.sync_values(),
memoizer.async_values()
memoizer.async_values(),
memoizer.blockers()
)
);
}

@ -25,6 +25,7 @@ import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { Program } from './visitors/Program.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -40,7 +41,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_component_renderer, create_async_block } from './visitors/shared/utils.js';
import { call_component_renderer } from './visitors/shared/utils.js';
/** @type {Visitors} */
const global_visitors = {
@ -53,6 +54,7 @@ const global_visitors = {
Identifier,
LabeledStatement,
MemberExpression,
Program,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
@ -103,7 +105,8 @@ export function server_component(analysis, options) {
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
state_fields: new Map(),
skip_hydration_boundaries: false
skip_hydration_boundaries: false,
is_instance: false
};
const module = /** @type {ESTree.Program} */ (
@ -113,7 +116,7 @@ export function server_component(analysis, options) {
const instance = /** @type {ESTree.Program} */ (
walk(
/** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes },
{ ...state, scopes: analysis.instance.scopes, is_instance: true },
{
...global_visitors,
ImportDeclaration(node) {
@ -243,10 +246,6 @@ export function server_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body)
]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments
component_block.loc = instance.loc;
@ -408,7 +407,8 @@ export function server_module(analysis, options) {
// to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map(),
state_fields: new Map()
state_fields: new Map(),
is_instance: false
};
const module = /** @type {ESTree.Program} */ (

@ -25,8 +25,12 @@ export function AwaitBlock(node, context) {
)
);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
if (node.metadata.expression.is_async()) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
node.metadata.expression.has_await
);
}
context.state.template.push(statement, block_close);

@ -34,7 +34,11 @@ export function EachBlock(node, context) {
const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;
each.push(...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body));
if (node.body)
each.push(
// TODO get rid of fragment.has_await
...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body)
);
const for_loop = b.for(
b.declaration('let', [
@ -65,8 +69,13 @@ export function EachBlock(node, context) {
block.body.push(for_loop);
}
if (node.metadata.expression.has_await) {
state.template.push(create_async_block(block), block_close);
const blockers = node.metadata.expression.blockers();
if (node.metadata.expression.has_await || blockers.elements.length > 0) {
state.template.push(
create_async_block(block, blockers, node.metadata.expression.has_await),
block_close
);
} else {
state.template.push(...block.body, block_close);
}

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { create_push } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -10,9 +11,6 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression));
const call = b.call('$.html', expression);
context.state.template.push(
node.metadata.expression.has_await
? b.stmt(b.call('$$renderer.push', b.thunk(call, true)))
: call
);
context.state.template.push(create_push(call, node.metadata.expression, true));
}

@ -23,12 +23,20 @@ export function IfBlock(node, context) {
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
if (
const is_async = node.metadata.expression.is_async();
const has_await =
node.metadata.expression.has_await ||
// TODO get rid of this stuff
node.consequent.metadata.has_await ||
node.alternate?.metadata.has_await
) {
statement = create_async_block(b.block([statement]));
node.alternate?.metadata.has_await;
if (is_async || has_await) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
!!has_await
);
}
context.state.template.push(statement, block_close);

@ -1,16 +1,22 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { empty_comment } from './shared/utils.js';
import { block_close, block_open, empty_comment } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
const is_async = node.metadata.expression.is_async();
if (is_async) context.state.template.push(block_open);
context.state.template.push(
empty_comment,
/** @type {BlockStatement} */ (context.visit(node.fragment)),
empty_comment
);
if (is_async) context.state.template.push(block_close);
}

@ -0,0 +1,29 @@
/** @import { Node, Program } from 'estree' */
/** @import { Context, ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { runes } from '../../../../state.js';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} node
* @param {Context} context
*/
export function Program(node, context) {
if (context.state.is_instance && runes) {
// @ts-ignore wtf
const c = /** @type {ComponentContext} */ (context);
return {
...node,
body: transform_body(
node,
c.state.analysis.awaited_statements,
b.id('$$renderer.run'),
(node) => /** @type {Node} */ (context.visit(node)),
(statement) => c.state.hoisted.push(statement)
)
};
}
context.next();
}

@ -3,32 +3,48 @@
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { empty_comment } from './shared/utils.js';
import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
const optimiser = new PromiseOptimiser();
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const snippet_function = /** @type {Expression} */ (context.visit(callee));
const snippet_function = optimiser.transform(
/** @type {Expression} */ (context.visit(callee)),
node.metadata.expression
);
const snippet_args = raw_args.map((arg) => {
return /** @type {Expression} */ (context.visit(arg));
const snippet_args = raw_args.map((arg, i) => {
return optimiser.transform(
/** @type {Expression} */ (context.visit(arg)),
node.metadata.arguments[i]
);
});
context.state.template.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$renderer'),
...snippet_args
)
let statement = b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$renderer'),
...snippet_args
)
);
if (optimiser.is_async()) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
context.state.template.push(statement);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}

@ -65,10 +65,13 @@ export function SlotElement(node, context) {
fallback
);
const statement =
optimiser.expressions.length > 0
? create_async_block(b.block([optimiser.apply(), b.stmt(slot)]))
: b.stmt(slot);
const statement = optimiser.is_async()
? create_async_block(
b.block([optimiser.apply(), b.stmt(slot)]),
optimiser.blockers(),
optimiser.has_await
)
: b.stmt(slot);
context.state.template.push(block_open, statement, block_close);
}

@ -6,7 +6,12 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js';
import {
build_template,
create_async_block,
create_child_block,
PromiseOptimiser
} from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -39,11 +44,14 @@ export function SvelteElement(node, context) {
const optimiser = new PromiseOptimiser();
/** @type {Statement[]} */
let statements = [];
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
context.state.template.push(
statements.push(
b.stmt(
b.call(
'$.push_element',
@ -74,9 +82,21 @@ export function SvelteElement(node, context) {
statement = create_child_block(b.block([optimiser.apply(), statement]), true);
}
context.state.template.push(statement);
statements.push(statement);
if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
statements.push(b.stmt(b.call('$.pop_element')));
}
if (node.metadata.expression.is_async()) {
statements = [
create_async_block(
b.block(statements),
node.metadata.expression.blockers(),
node.metadata.expression.has_await
)
];
}
context.state.template.push(...statements);
}

@ -5,8 +5,7 @@ import {
empty_comment,
build_attribute_value,
create_async_block,
PromiseOptimiser,
build_template
PromiseOptimiser
} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
@ -323,8 +322,12 @@ export function build_inline_component(node, expression, context) {
);
}
if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
if (optimiser.is_async()) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
if (dynamic && custom_css_props.length === 0) {

@ -1,5 +1,5 @@
/** @import { Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Expression, Identifier, Node, Statement, BlockStatement, ArrayExpression } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
@ -77,12 +77,11 @@ export function process_children(nodes, { visit, state }) {
}
for (const node of nodes) {
if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
if (node.type === 'ExpressionTag' && node.metadata.expression.is_async()) {
flush();
const visited = /** @type {Expression} */ (visit(node.expression));
state.template.push(
b.stmt(b.call('$$renderer.push', b.thunk(b.call('$.escape', visited), true)))
);
const expression = /** @type {Expression} */ (visit(node.expression));
state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression));
} else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
@ -275,9 +274,40 @@ export function create_child_block(body, async) {
/**
* Creates a `$$renderer.async(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {ArrayExpression} blockers
* @param {boolean} has_await
* @param {boolean} markers
*/
export function create_async_block(body) {
return b.stmt(b.call('$$renderer.async', b.arrow([b.id('$$renderer')], body, true)));
export function create_async_block(body, blockers = b.array([]), has_await = true, markers = true) {
return b.stmt(
b.call(
markers ? '$$renderer.async_block' : '$$renderer.async',
blockers,
b.arrow([b.id('$$renderer')], body, has_await)
)
);
}
/**
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {boolean} markers
* @returns {Expression | Statement}
*/
export function create_push(expression, metadata, markers = false) {
if (metadata.is_async()) {
let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await)));
const blockers = metadata.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(b.block([statement]), blockers, false, markers);
}
return statement;
}
return expression;
}
/**
@ -295,13 +325,26 @@ export class PromiseOptimiser {
/** @type {Expression[]} */
expressions = [];
has_await = false;
/** @type {Set<Expression>} */
#blockers = new Set();
/**
*
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
transform = (expression, metadata) => {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
if (metadata.has_await) {
this.has_await = true;
const length = this.expressions.push(expression);
return b.id(`$$${length - 1}`);
}
@ -310,6 +353,10 @@ export class PromiseOptimiser {
};
apply() {
if (this.expressions.length === 0) {
return b.empty;
}
if (this.expressions.length === 1) {
return b.const('$$0', this.expressions[0]);
}
@ -327,4 +374,12 @@ export class PromiseOptimiser {
b.await(b.call('Promise.all', promises))
);
}
blockers() {
return b.array([...this.#blockers]);
}
is_async() {
return this.expressions.length > 0 || this.#blockers.size > 0;
}
}

@ -0,0 +1,207 @@
/** @import * as ESTree from 'estree' */
/** @import { AwaitedStatement } from '../../types' */
import * as b from '#compiler/builders';
// TODO find a way to DRY out this and the corresponding server visitor
/**
* @param {ESTree.Program} program
* @param {Map<ESTree.Node, AwaitedStatement>} awaited_statements
* @param {ESTree.Expression} runner
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @param {(node: ESTree.Statement | ESTree.ModuleDeclaration) => void} hoist
*/
export function transform_body(program, awaited_statements, runner, transform, hoist) {
/** @type {ESTree.Statement[]} */
const out = [];
/** @type {AwaitedStatement[]} */
const statements = [];
/** @type {AwaitedStatement[]} */
const deriveds = [];
let awaited = false;
/**
* @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.ClassDeclaration | ESTree.FunctionDeclaration} node
*/
const push = (node) => {
const statement = awaited_statements.get(node);
awaited ||= !!statement?.has_await;
if (!awaited || !statement || node.type === 'FunctionDeclaration') {
if (node.type === 'VariableDeclarator') {
out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init))));
} else {
out.push(/** @type {ESTree.Statement} */ (transform(node)));
}
return;
}
// TODO put deriveds into a separate array, and group them immediately
// after their latest dependency. for now, to avoid having to figure
// out the intricacies of dependency tracking, just let 'em waterfall
// if (node.type === 'VariableDeclarator') {
// const rune = get_rune(node.init, context.state.scope);
// if (rune === '$derived' || rune === '$derived.by') {
// deriveds.push(statement);
// return;
// }
// }
statements.push(statement);
};
for (let node of program.body) {
if (node.type === 'ImportDeclaration') {
// TODO we can get rid of the visitor
hoist(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// this can't happen, but it's useful for TypeScript to understand that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
// TODO ditto — no visitor needed
node = node.declaration;
} else {
continue;
}
}
if (node.type === 'VariableDeclaration') {
if (
!awaited &&
node.declarations.every((declarator) => !awaited_statements.get(declarator)?.has_await)
) {
out.push(/** @type {ESTree.VariableDeclaration} */ (transform(node)));
} else {
for (const declarator of node.declarations) {
push(declarator);
}
}
} else {
push(node);
}
}
for (const derived of deriveds) {
// find the earliest point we can insert this derived
let index = -1;
for (const binding of derived.reads) {
index = Math.max(
index,
statements.findIndex((s) => s.declarations.includes(binding))
);
}
if (index === -1 && !derived.has_await) {
const node = /** @type {ESTree.VariableDeclarator} */ (derived.node);
out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init))));
} else {
// TODO combine deriveds with Promise.all where necessary
statements.splice(index + 1, 0, derived);
}
}
var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict
if (statements.length > 0) {
var declarations = statements.map((s) => s.declarations).flat();
if (declarations.length > 0) {
out.push(
b.declaration(
'var',
declarations.map((d) => b.declarator(d.node))
)
);
}
const thunks = statements.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration} */ (
transform(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
]),
s.has_await
);
}
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
}
if (s.node.type === 'FunctionDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' })
),
s.has_await
);
}
if (s.node.type === 'ExpressionStatement') {
const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression));
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
}
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
});
out.push(b.var(promises, b.call(runner, b.array(thunks))));
for (let i = 0; i < statements.length; i += 1) {
const s = statements[i];
var blocker = b.member(promises, b.literal(i), true);
for (const binding of s.declarations) {
binding.blocker = blocker;
}
for (const binding of s.writes) {
// if a statement writes to a binding, any reads of that
// binding must wait for the statement
binding.blocker = blocker;
}
}
}
return out;
}

@ -8,5 +8,8 @@ export interface TransformState {
readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, Scope>;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly state_fields: Map<string, StateField>;
}

@ -1,5 +1,6 @@
/** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
import * as b from '#compiler/builders';
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
@ -91,6 +92,29 @@ export class ExpressionMetadata {
* @type {Set<Binding>}
*/
references = new Set();
/** @type {null | Set<Expression>} */
#blockers = null;
#get_blockers() {
if (!this.#blockers) {
this.#blockers = new Set();
for (const d of this.dependencies) {
if (d.blocker) this.#blockers.add(d.blocker);
}
}
return this.#blockers;
}
blockers() {
return b.array([...this.#get_blockers()]);
}
is_async() {
return this.has_await || this.#get_blockers().size > 0;
}
}
/**

@ -108,6 +108,9 @@ export class Binding {
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/** @type {Array<{ value: Expression; scope: Scope }>} */
assignments = [];
/**
* For `legacy_reactive`: its reactive dependencies
* @type {Binding[]}
@ -129,6 +132,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
@ -143,6 +155,10 @@ export class Binding {
this.initial = initial;
this.kind = kind;
this.declaration_kind = declaration_kind;
if (initial) {
this.assignments.push({ value: /** @type {Expression} */ (initial), scope });
}
}
get updated() {
@ -859,7 +875,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
const references = [];
/** @type {[Scope, Pattern | MemberExpression][]} */
/** @type {[Scope, Pattern | MemberExpression, Expression][]} */
const updates = [];
/**
@ -1047,12 +1063,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// updates
AssignmentExpression(node, { state, next }) {
updates.push([state.scope, node.left]);
updates.push([state.scope, node.left, node.right]);
next();
},
UpdateExpression(node, { state, next }) {
updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]);
const expression = /** @type {Identifier | MemberExpression} */ (node.argument);
updates.push([state.scope, expression, expression]);
next();
},
@ -1273,10 +1290,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
BindDirective(node, context) {
updates.push([
context.state.scope,
/** @type {Identifier | MemberExpression} */ (node.expression)
]);
if (node.expression.type !== 'SequenceExpression') {
const expression = /** @type {Identifier | MemberExpression} */ (node.expression);
updates.push([context.state.scope, expression, expression]);
}
context.next();
},
@ -1311,7 +1329,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scope.reference(node, path);
}
for (const [scope, node] of updates) {
for (const [scope, node, value] of updates) {
for (const expression of unwrap_pattern(node)) {
const left = object(expression);
const binding = left && scope.get(left.name);
@ -1319,6 +1337,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (binding !== null && left !== binding.node) {
if (left === expression) {
binding.reassigned = true;
binding.assignments.push({ value, scope });
} else {
binding.mutated = true;
}

@ -3,11 +3,18 @@ import type {
AwaitExpression,
CallExpression,
ClassBody,
ClassDeclaration,
FunctionDeclaration,
Identifier,
LabeledStatement,
Program
ModuleDeclaration,
Pattern,
Program,
Statement,
VariableDeclarator
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { ExpressionMetadata } from './nodes.js';
export interface Js {
ast: Program;
@ -27,6 +34,22 @@ export interface ReactiveStatement {
dependencies: Binding[];
}
export interface AwaitedDeclaration {
id: Identifier;
has_await: boolean;
pattern: Pattern;
metadata: ExpressionMetadata;
updated_by: Set<Identifier>;
}
export interface AwaitedStatement {
node: Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration;
has_await: boolean;
declarations: Binding[];
reads: Set<Binding>;
writes: Set<Binding>;
}
/**
* Analysis common to modules and components
*/
@ -108,4 +131,9 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
/**
* 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, AwaitedStatement>;
}

@ -48,6 +48,7 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
/** @deprecated we should get rid of this in favour of the `$$renderer.run` mechanism */
has_await: boolean;
};
}

@ -1,4 +1,4 @@
/** @import { AST } from '#compiler' */
/** @import { AST, Scope } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';

@ -82,6 +82,17 @@ export function block(body) {
return { type: 'BlockStatement', body };
}
/**
* @param {ESTree.Identifier | null} id
* @param {ESTree.ClassBody} body
* @param {ESTree.Expression | null} [superClass]
* @param {ESTree.Decorator[]} [decorators]
* @returns {ESTree.ClassExpression}
*/
export function class_expression(id, body, superClass, decorators = []) {
return { type: 'ClassExpression', body, superClass, decorators };
}
/**
* @param {string} name
* @param {ESTree.Statement} body
@ -184,7 +195,7 @@ export function declaration(kind, declarations) {
/**
* @param {ESTree.Pattern | string} pattern
* @param {ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclarator}
*/
export function declarator(pattern, init) {
@ -520,7 +531,7 @@ const this_instance = {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclaration}
*/
function let_builder(pattern, init) {
@ -529,7 +540,7 @@ function let_builder(pattern, init) {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} init
* @param {ESTree.Expression | null} init
* @returns {ESTree.VariableDeclaration}
*/
function const_builder(pattern, init) {
@ -538,7 +549,7 @@ function const_builder(pattern, init) {
/**
* @param {string | ESTree.Pattern} pattern
* @param { ESTree.Expression} [init]
* @param {ESTree.Expression | null} [init]
* @returns {ESTree.VariableDeclaration}
*/
function var_builder(pattern, init) {

@ -14,10 +14,11 @@ import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<Promise<void>>} blockers
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
export function async(node, blockers = [], 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(blockers, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);

@ -480,6 +480,7 @@ function set_attributes(
/**
* @param {Element & ElementCSSInlineStyle} element
* @param {Array<Promise<void>>} blockers
* @param {(...expressions: any) => Record<string | symbol, any>} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
@ -492,11 +493,12 @@ export function attribute_effect(
fn,
sync = [],
async = [],
blockers = [],
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
flatten(sync, async, (values) => {
flatten(blockers, sync, async, (values) => {
/** @type {Record<string | symbol, any> | undefined} */
var prev = undefined;

@ -100,6 +100,7 @@ export {
export {
async_body,
for_await_track_reactivity_loss,
run,
save,
track_reactivity_loss
} from './reactivity/async.js';

@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode, Value } from '#client' */
import { DESTROYED } from '#client/constants';
import { DESTROYED, STALE_REACTION } from '#client/constants';
import { DEV } from 'esm-env';
import {
component_context,
@ -33,18 +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 {Array<Promise<void>>} blockers
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
*/
export function flatten(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;
}
@ -56,29 +56,46 @@ export function flatten(sync, async, fn) {
var was_hydrating = hydrating;
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
restore();
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.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);
}
}
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);
if (blockers.length > 0) {
Promise.all(blockers).then(() => {
restore();
try {
return run();
} finally {
batch?.deactivate();
unset_context();
}
});
} else {
run();
}
}
/**
@ -259,3 +276,83 @@ export async function async_body(anchor, fn) {
unset_context();
}
}
/**
* @param {Array<() => void | Promise<void>>} thunks
*/
export function run(thunks) {
const restore = capture();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
batch.increment(blocking);
var active = /** @type {Effect} */ (active_effect);
/** @type {null | { error: any }} */
var errored = null;
let was_hydrating = hydrating;
/** @param {any} error */
const handle_error = (error) => {
errored = { error }; // wrap in object in case a promise rejects with a falsy value
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
};
var promise = Promise.resolve(thunks[0]()).catch(handle_error);
var promises = [promise];
for (const fn of thunks.slice(1)) {
promise = promise
.then(() => {
if (errored) {
throw errored.error;
}
if (aborted(active)) {
throw STALE_REACTION;
}
try {
restore();
return fn();
} finally {
// TODO do we need it here as well as below?
unset_context();
if (was_hydrating) {
set_hydrating(false);
}
}
})
.catch(handle_error)
.finally(() => {
unset_context();
if (was_hydrating) {
set_hydrating(false);
}
});
promises.push(promise);
}
promise
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.then(() => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
});
return promises;
}

@ -365,9 +365,10 @@ export function render_effect(fn, flags = 0) {
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
*/
export function template_effect(fn, sync = [], async = []) {
flatten(sync, async, (values) => {
export function template_effect(fn, sync = [], async = [], blockers = []) {
flatten(blockers, sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
});
}

@ -34,7 +34,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { Batch, current_batch, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';

@ -100,14 +100,69 @@ export class Renderer {
}
/**
* @param {Array<Promise<void>>} blockers
* @param {(renderer: Renderer) => void} fn
*/
async(fn) {
async_block(blockers, fn) {
this.#out.push(BLOCK_OPEN);
this.child(fn);
this.async(blockers, fn);
this.#out.push(BLOCK_CLOSE);
}
/**
* @param {Array<Promise<void>>} blockers
* @param {(renderer: Renderer) => void} fn
*/
async(blockers, fn) {
let callback = fn;
if (blockers.length > 0) {
const context = ssr_context;
callback = (renderer) => {
return Promise.all(blockers).then(() => {
const previous_context = ssr_context;
try {
set_ssr_context(context);
return fn(renderer);
} finally {
set_ssr_context(previous_context);
}
});
};
}
this.child(callback);
}
/**
* @param {Array<() => void>} thunks
*/
run(thunks) {
const context = ssr_context;
let promise = Promise.resolve(thunks[0]());
const promises = [promise];
for (const fn of thunks.slice(1)) {
promise = promise.then(() => {
const previous_context = ssr_context;
set_ssr_context(context);
try {
return fn();
} finally {
set_ssr_context(previous_context);
}
});
promises.push(promise);
}
return promises;
}
/**
* Create a child renderer. The child renderer inherits the state from the parent,
* but has its own content.

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip_mode: ['server'],
ssrHtml: '<p>yep</p>',
async test({ assert, target, variant }) {
if (variant === 'dom') {
await tick();
}
assert.htmlEqual(target.innerHTML, '<p>yep</p>');
}
});

@ -0,0 +1,10 @@
<script>
await 0;
const condition = await true;
</script>
{#if condition}
<p>yep</p>
{:else}
<p>nope</p>
{/if}

@ -6,7 +6,7 @@ export default function Async_each_fallback_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve([])], (node, $$collection) => {
$.async(node, [], [() => Promise.resolve([])], (node, $$collection) => {
$.each(
node,
16,

@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_each_fallback_hoisting($$renderer) {
$$renderer.async(async ($$renderer) => {
$$renderer.async_block([], async ($$renderer) => {
const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))());
if (each_array.length !== 0) {

@ -9,7 +9,7 @@ export default function Async_each_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => {
$.async(node, [], [() => Promise.resolve([first, second, third])], (node, $$collection) => {
$.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => {
$.next();

@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) {
$$renderer.push(`<!--[-->`);
$$renderer.async(async ($$renderer) => {
$$renderer.async_block([], async ($$renderer) => {
const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))());
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {

@ -6,7 +6,7 @@ export default function Async_if_alternate_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve(false)], (node, $$condition) => {
$.async(node, [], [() => Promise.resolve(false)], (node, $$condition) => {
var consequent = ($$anchor) => {
var text = $.text();

@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_alternate_hoisting($$renderer) {
$$renderer.async(async ($$renderer) => {
$$renderer.async_block([], async ($$renderer) => {
if ((await $.save(Promise.resolve(false)))()) {
$$renderer.push('<!--[-->');
$$renderer.push(async () => $.escape(await Promise.reject('no no no')));

@ -6,7 +6,7 @@ export default function Async_if_hoisting($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.async(node, [() => Promise.resolve(true)], (node, $$condition) => {
$.async(node, [], [() => Promise.resolve(true)], (node, $$condition) => {
var consequent = ($$anchor) => {
var text = $.text();

@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_hoisting($$renderer) {
$$renderer.async(async ($$renderer) => {
$$renderer.async_block([], async ($$renderer) => {
if ((await $.save(Promise.resolve(true)))()) {
$$renderer.push('<!--[-->');
$$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes')));

@ -5,48 +5,47 @@ import * as $ from 'svelte/internal/client';
export default function Async_in_derived($$anchor, $$props) {
$.push($$props, true);
$.async_body($$anchor, async ($$anchor) => {
let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
var yes1, yes2, no1, no2;
let no1 = $.derived(async () => {
return await 1;
});
var $$promises = $.run([
async () => yes1 = await $.async_derived(() => 1),
async () => yes2 = await $.async_derived(async () => foo(await 1)),
let no2 = $.derived(() => async () => {
() => no1 = $.derived(async () => {
return await 1;
});
}),
if ($.aborted()) return;
var fragment = $.comment();
var node = $.first_child(fragment);
() => no2 = $.derived(() => async () => {
return await 1;
})
]);
{
var consequent = ($$anchor) => {
$.async_body($$anchor, async ($$anchor) => {
const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
var fragment = $.comment();
var node = $.first_child(fragment);
const no1 = $.derived(() => (async () => {
return await 1;
})());
{
var consequent = ($$anchor) => {
$.async_body($$anchor, async ($$anchor) => {
const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
const no2 = $.derived(() => (async () => {
return await 1;
})());
const no1 = $.derived(() => (async () => {
return await 1;
})());
if ($.aborted()) return;
});
};
const no2 = $.derived(() => (async () => {
return await 1;
})());
$.if(node, ($$render) => {
if (true) $$render(consequent);
if ($.aborted()) return;
});
}
};
$.append($$anchor, fragment);
});
$.if(node, ($$render) => {
if (true) $$render(consequent);
});
}
$.append($$anchor, fragment);
$.pop();
}

@ -3,38 +3,40 @@ import * as $ from 'svelte/internal/server';
export default function Async_in_derived($$renderer, $$props) {
$$renderer.component(($$renderer) => {
$$renderer.async(async ($$renderer) => {
let yes1 = (await $.save(1))();
let yes2 = foo((await $.save(1))());
var yes1, yes2, no1, no2;
let no1 = (async () => {
return await 1;
})();
var $$promises = $$renderer.run([
async () => yes1 = await 1,
async () => yes2 = foo(await 1),
let no2 = async () => {
() => no1 = (async () => {
return await 1;
};
$$renderer.async(async ($$renderer) => {
if (true) {
$$renderer.push('<!--[-->');
const yes1 = (await $.save(1))();
const yes2 = foo((await $.save(1))());
const no1 = (async () => {
return await 1;
})();
})(),
const no2 = (async () => {
return await 1;
})();
} else {
$$renderer.push('<!--[!-->');
}
});
$$renderer.push(`<!--]-->`);
() => no2 = async () => {
return await 1;
}
]);
$$renderer.async_block([], async ($$renderer) => {
if (true) {
$$renderer.push('<!--[-->');
const yes1 = (await $.save(1))();
const yes2 = foo((await $.save(1))());
const no1 = (async () => {
return await 1;
})();
const no2 = (async () => {
return await 1;
})();
} else {
$$renderer.push('<!--[!-->');
}
});
$$renderer.push(`<!--]-->`);
});
}
Loading…
Cancel
Save