feat: out of order rendering (#17038)

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* revert

* note to self

* unused

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* deprecate

* update tests

* lint

* lint

* WIP

* WIP

* fix

* WIP

* unused

* deopt to ensure state is ready

* fix

* DRY

* reduce diff

* reduce diff

* reduce diff

* handle blocked attributes

* WIP

* pre-transform

* tidy up

* fix

* WIP

* WIP

* fix: handle `<svelte:head>` rendered asynchronously

* fix tests

* fix

* delay resolve

* Revert "fix"

This reverts commit 2e56cd7575.

* add error

* simplify/fix hydration restoration

* fix

* use $state.eager mechanism for $effect.pending - way simpler and more robust

* disable these warnings for now, too many false positives

* fix

* changeset was already merged

* changeset

* oops

* lint

* docs + tidy

* prettier

* robustify: logic inside memoizer and outside could get out of sync, introducing bugs. use equality comparison instead

* oops

* uncomment

* use finally

* use is_async

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/17055/head
Rich Harris 2 weeks ago committed by GitHub
parent 90a8a03988
commit 1126ef3186
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: out-of-order rendering

@ -6,7 +6,12 @@ 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,13 @@ 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(),
instance_body: {
sync: [],
async: [],
declarations: [],
hoisted: []
}
};
if (!runes) {
@ -676,6 +687,194 @@ export function analyze_component(root, source, options) {
}
}
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
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 as they only run once async work has completed
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);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
var promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Expression} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (awaited && node.type !== 'FunctionDeclaration') {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
} else if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
if (props_refs) {

@ -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;
}

@ -172,6 +172,7 @@ export function BindDirective(node, context) {
}
const binding = context.state.scope.get(left.name);
node.metadata.binding = binding;
if (assignee.type === 'Identifier') {
// reassignment

@ -81,8 +81,13 @@ export function SnippetBlock(node, context) {
function can_hoist_snippet(scope, scopes, visited = new Set()) {
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (!binding) continue;
if (!binding || binding.scope.function_depth === 0) {
if (binding.blocker) {
return false;
}
if (binding.scope.function_depth === 0) {
continue;
}

@ -33,7 +33,6 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
@ -111,7 +110,6 @@ const visitors = {
HtmlTag,
Identifier,
IfBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
LetDirective,
@ -153,7 +151,7 @@ export function client_component(analysis, options) {
scope: analysis.module.scope,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
@ -370,29 +368,11 @@ 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)));
}
@ -403,7 +383,6 @@ export function client_component(analysis, options) {
}
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
if (analysis.needs_mutation_validation) {
component_block.body.unshift(

@ -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,
{

@ -243,18 +243,29 @@ export function BindDirective(node, context) {
}
}
const defer =
node.name !== 'this' &&
parent.type === 'RegularElement' &&
parent.attributes.find((a) => a.type === 'UseDirective');
let statement = defer ? b.stmt(b.call('$.effect', b.thunk(call))) : b.stmt(call);
// TODO this doesn't account for function bindings
if (node.metadata.binding?.blocker) {
statement = b.stmt(
b.call(b.member(node.metadata.binding.blocker, b.id('then')), b.thunk(b.block([statement])))
);
}
// Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions.
// bind:this is a special case as it's one-way and could influence the render effect.
if (node.name === 'this') {
context.state.init.push(b.stmt(call));
context.state.init.push(statement);
} else {
const has_use =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
if (has_use) {
context.state.init.push(b.stmt(b.call('$.effect', b.thunk(call))));
if (defer) {
context.state.init.push(statement);
} else {
context.state.after_update.push(b.stmt(call));
context.state.after_update.push(statement);
}
}
}

@ -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';
@ -80,7 +80,7 @@ export function CallExpression(node, context) {
);
case '$effect.pending':
return b.call('$.pending');
return b.call('$.eager', b.thunk(b.call('$.pending')));
case '$inspect':
case '$inspect().with':

@ -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))
)
)

@ -1,16 +0,0 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
/**
* @param {ImportDeclaration} node
* @param {ComponentContext} context
*/
export function ImportDeclaration(node, context) {
if ('hoisted' in context.state) {
context.state.hoisted.push(node);
return b.empty;
}
context.next();
}

@ -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,15 @@
/** @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 { 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 +138,16 @@ export function Program(_, context) {
add_state_transformers(context);
if (context.state.is_instance) {
return {
...node,
body: transform_body(
context.state.analysis.instance_body,
b.id('$.run'),
(node) => /** @type {Node} */ (context.visit(node))
)
};
}
context.next();
}

@ -22,11 +22,11 @@ export function RenderTag(node, context) {
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
let expression = build_expression(context, arg, metadata);
const memoized = memoizer.add(expression, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata));
if (expression !== memoized) {
expression = b.call('$.get', memoized);
}
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 = [];
@ -129,14 +129,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 memoized_expression = memoizer.add(expression, attribute.metadata.expression);
const is_memoized = expression !== memoized_expression;
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
if (
is_memoized ||
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))
: expression
)
b.thunk(is_memoized ? b.call('$.get', memoized_expression) : expression)
);
} else {
props_and_spreads.push(expression);
@ -147,10 +149,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 value !== memoized ? b.call('$.get', memoized) : value;
}).value
)
);
@ -184,9 +186,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 value !== memoized ? b.call('$.get', memoized) : value;
}
);
@ -497,12 +499,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
@ -95,7 +97,7 @@ export function server_component(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
hoisted: [b.import_all('$', 'svelte/internal/server')],
hoisted: [b.import_all('$', 'svelte/internal/server'), ...analysis.instance_body.hoisted],
legacy_reactive_statements: new Map(),
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
@ -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);

@ -12,7 +12,14 @@ import { get_inspect_args } from '../../utils.js';
export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
if (
rune === '$host' ||
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$inspect.trace'
) {
// we will only encounter `$effect` etc if they are top-level statements in the <script>
// following an `await`, otherwise they are removed by the ExpressionStatement visitor
return b.void0;
}

@ -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,15 @@ 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);
if (node.metadata.expression.is_async()) {
state.template.push(
create_async_block(
block,
node.metadata.expression.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,25 @@
/** @import { Node, Program } from 'estree' */
/** @import { Context, ComponentServerTransformState } from '../types' */
import * as b from '#compiler/builders';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} node
* @param {Context} context
*/
export function Program(node, context) {
if (context.state.is_instance) {
const state = /** @type {ComponentServerTransformState} */ (context.state);
return {
...node,
body: transform_body(
state.analysis.instance_body,
b.id('$$renderer.run'),
(node) => /** @type {Node} */ (context.visit(node))
)
};
}
context.next();
}

@ -12,7 +12,8 @@ import {
process_children,
build_template,
create_child_block,
PromiseOptimiser
PromiseOptimiser,
create_async_block
} from './shared/utils.js';
/**
@ -202,13 +203,19 @@ export function RegularElement(node, context) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
if (optimiser.is_async()) {
let statement = create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
const blockers = optimiser.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(b.block([statement]), blockers, false, false);
}
context.state.template.push(statement);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);

@ -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(
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,9 +65,12 @@ export function SlotElement(node, context) {
fallback
);
const statement =
optimiser.expressions.length > 0
? create_async_block(b.block([optimiser.apply(), 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,14 @@ export function build_inline_component(node, expression, context) {
);
}
if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
const is_async = optimiser.is_async();
if (is_async) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
optimiser.blockers(),
optimiser.has_await
);
}
if (dynamic && custom_css_props.length === 0) {
@ -334,6 +339,7 @@ export function build_inline_component(node, expression, context) {
context.state.template.push(statement);
if (
!is_async &&
!context.state.skip_hydration_boundaries &&
custom_css_props.length === 0 &&
optimiser.expressions.length === 0

@ -1,4 +1,4 @@
/** @import { Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { Expression, Identifier, Node, Statement, BlockStatement, ArrayExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.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,50 @@ 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} needs_hydration_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,
needs_hydration_markers = true
) {
return b.stmt(
b.call(
needs_hydration_markers ? '$$renderer.async_block' : '$$renderer.async',
blockers,
b.arrow([b.id('$$renderer')], body, has_await)
)
);
}
/**
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {boolean} needs_hydration_markers
* @returns {Expression | Statement}
*/
export function create_push(expression, metadata, needs_hydration_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,
needs_hydration_markers
);
}
return statement;
}
return expression;
}
/**
@ -291,17 +331,36 @@ export function call_component_renderer(body, component_fn_id) {
);
}
/**
* A utility for optimising promises in templates. Without it code like
* `<Component foo={await fetch()} bar={await other()} />` would be transformed
* into two blocking promises, with it it's using `Promise.all` to await them.
* It also keeps track of blocking promises, i.e. those that need to be resolved before continuing.
*/
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 +369,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 +390,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,102 @@
/** @import * as ESTree from 'estree' */
/** @import { ComponentAnalysis } from '../../types' */
import * as b from '#compiler/builders';
/**
* Transforms the body of the instance script in such a way that await expressions are made non-blocking as much as possible.
*
* Example Transformation:
* ```js
* let x = 1;
* let data = await fetch('/api');
* let y = data.value;
* ```
* becomes:
* ```js
* let x = 1;
* var data, y;
* var $$promises = $.run([
* () => data = await fetch('/api'),
* () => y = data.value
* ]);
* ```
* where `$$promises` is an array of promises that are resolved in the order they are declared,
* and which expressions in the template can await on like `await $$promises[0]` which means they
* wouldn't have to wait for e.g. `$$promises[1]` to resolve.
*
* @param {ComponentAnalysis['instance_body']} instance_body
* @param {ESTree.Expression} runner
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @returns {Array<ESTree.Statement | ESTree.VariableDeclaration>}
*/
export function transform_body(instance_body, runner, transform) {
// Any sync statements before the first await expression
const statements = instance_body.sync.map(
(node) => /** @type {ESTree.Statement | ESTree.VariableDeclaration} */ (transform(node))
);
// Declarations for the await expressions (they will asign to them; need to be hoisted to be available in whole instance scope)
if (instance_body.declarations.length > 0) {
statements.push(
b.declaration(
'var',
instance_body.declarations.map((id) => b.declarator(id))
)
);
}
// Thunks for the await expressions
if (instance_body.async.length > 0) {
const thunks = instance_body.async.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 === '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);
});
// TODO get the `$$promises` ID from scope
statements.push(b.var('$$promises', b.call(runner, b.array(thunks))));
}
return statements;
}

@ -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,19 @@ import type {
AwaitExpression,
CallExpression,
ClassBody,
ClassDeclaration,
FunctionDeclaration,
Identifier,
LabeledStatement,
Program
ModuleDeclaration,
Pattern,
Program,
Statement,
VariableDeclaration,
VariableDeclarator
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { ExpressionMetadata } from './nodes.js';
export interface Js {
ast: Program;
@ -27,6 +35,14 @@ export interface ReactiveStatement {
dependencies: Binding[];
}
export interface AwaitedDeclaration {
id: Identifier;
has_await: boolean;
pattern: Pattern;
metadata: ExpressionMetadata;
updated_by: Set<Identifier>;
}
/**
* Analysis common to modules and components
*/
@ -108,4 +124,13 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
/**
* Pre-transformed `<script>` block
*/
instance_body: {
hoisted: Array<Statement | ModuleDeclaration>;
sync: Array<Statement | ModuleDeclaration | VariableDeclaration>;
async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>;
declarations: Array<Identifier>;
};
}

@ -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;
};
}
@ -206,6 +207,7 @@ export namespace AST {
expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */
metadata: {
binding?: Binding | null;
binding_group_name: Identifier;
parent_each_blocks: EachBlock[];
};

@ -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);
@ -47,12 +48,12 @@ export function async(node, expressions, fn) {
fn(node, ...values);
} finally {
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
});
}

@ -34,7 +34,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { Batch } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -110,12 +110,6 @@ export class Boundary {
*/
#effect_pending = null;
#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
};
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#local_pending_count);
@ -329,7 +323,10 @@ export class Boundary {
this.#update_pending_count(d);
this.#local_pending_count += d;
effect_pending_updates.add(this.#effect_pending_update);
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#local_pending_count);
}
}
get_effect_pending() {

@ -483,6 +483,7 @@ function set_attributes(
* @param {(...expressions: any) => Record<string | symbol, any>} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
@ -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,
@ -25,25 +25,18 @@ import {
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../dom/hydration.js';
import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.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;
}
@ -53,8 +46,7 @@ export function flatten(sync, async, fn) {
var restore = capture();
var was_hydrating = hydrating;
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
restore();
@ -68,10 +60,6 @@ export function flatten(sync, async, fn) {
}
}
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate();
unset_context();
})
@ -80,6 +68,22 @@ export function flatten(sync, async, fn) {
});
}
if (blockers.length > 0) {
Promise.all(blockers).then(() => {
restore();
try {
return run();
} finally {
batch?.deactivate();
unset_context();
}
});
} else {
run();
}
}
/**
* Captures the current effect context so that we can restore it after
* some asynchronous work has happened (so that e.g. `await a + b`
@ -91,12 +95,6 @@ export function capture() {
var previous_component_context = component_context;
var previous_batch = current_batch;
var was_hydrating = hydrating;
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
}
if (DEV) {
var previous_dev_stack = dev_stack;
}
@ -107,11 +105,6 @@ export function capture() {
set_component_context(previous_component_context);
if (activate_batch) previous_batch?.activate();
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
if (DEV) {
set_from_async_derived(null);
set_dev_stack(previous_dev_stack);
@ -248,13 +241,79 @@ export async function async_body(anchor, fn) {
invoke_error_boundary(error, active);
}
} finally {
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
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;
/** @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();
}
})
.catch(handle_error)
.finally(() => {
unset_context();
});
promises.push(promise);
}
promise
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
});
return promises;
}

@ -74,9 +74,6 @@ export let previous_batch = null;
*/
export let batch_values = null;
/** @type {Set<() => void>} */
export let effect_pending_updates = new Set();
/** @type {Effect[]} */
let queued_root_effects = [];
@ -324,16 +321,6 @@ export class Batch {
}
this.deactivate();
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);
update();
if (current_batch !== null) {
// only do one at a time
break;
}
}
}
discard() {

@ -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);
});
}

@ -602,18 +602,19 @@ export function get(signal) {
}
if (DEV) {
if (current_async_effect) {
var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
var was_read = current_async_effect.deps?.includes(signal);
// TODO reinstate this, but make it actually work
// if (current_async_effect) {
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
// var was_read = current_async_effect.deps?.includes(signal);
if (!tracking && !untracking && !was_read) {
w.await_reactivity_loss(/** @type {string} */ (signal.label));
// if (!tracking && !untracking && !was_read) {
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
var trace = get_stack('traced at');
// eslint-disable-next-line no-console
if (trace) console.warn(trace);
}
}
// var trace = get_stack('traced at');
// // eslint-disable-next-line no-console
// if (trace) console.warn(trace);
// }
// }
recent_async_deriveds.delete(signal);
@ -628,7 +629,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
trace = get_stack('traced at');
var trace = get_stack('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);

@ -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.

@ -12,6 +12,7 @@ import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
import { clear } from '../../src/internal/client/reactivity/batch.js';
import { hydrating } from '../../src/internal/client/dom/hydration.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
@ -533,6 +534,10 @@ async function run_test_variant(
throw err;
}
} finally {
if (hydrating) {
throw new Error('Hydration state was not cleared');
}
config.after_test?.();
// Free up the microtask queue

@ -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}

@ -2,6 +2,9 @@ import { tick } from 'svelte';
import { test } from '../../test';
export default test({
// TODO reinstate
skip: true,
compileOptions: {
dev: true
},

@ -2,6 +2,9 @@ import { tick } from 'svelte';
import { test } from '../../test';
export default test({
// TODO reinstate this
skip: true,
compileOptions: {
dev: true
},

@ -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,19 +5,20 @@ 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;
() => no2 = $.derived(() => async () => {
return await 1;
})
]);
var fragment = $.comment();
var node = $.first_child(fragment);
@ -46,7 +47,5 @@ export default function Async_in_derived($$anchor, $$props) {
}
$.append($$anchor, fragment);
});
$.pop();
}

@ -3,19 +3,22 @@ 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 () => {
var $$promises = $$renderer.run([
async () => yes1 = await 1,
async () => yes2 = foo(await 1),
() => no1 = (async () => {
return await 1;
})();
})(),
let no2 = async () => {
() => no2 = async () => {
return await 1;
};
}
]);
$$renderer.async(async ($$renderer) => {
$$renderer.async_block([], async ($$renderer) => {
if (true) {
$$renderer.push('<!--[-->');
@ -36,5 +39,4 @@ export default function Async_in_derived($$renderer, $$props) {
$$renderer.push(`<!--]-->`);
});
});
}
Loading…
Cancel
Save