out-of-order-rendering
Rich Harris 4 days ago
parent 795fa1ab25
commit 67ee1ba133

@ -22,6 +22,25 @@ export function IfBlock(node, context) {
expression: node.metadata.expression
});
// TODO this can be helperised
for (const binding of node.metadata.expression.dependencies) {
const awaited = context.state.analysis.awaited_declarations.get(binding.node.name);
if (awaited) {
node.metadata.async ??= {
declarations: new Set()
};
node.metadata.async.declarations.add(awaited);
}
}
if (node.metadata.expression.has_await) {
node.metadata.async ??= {
declarations: new Set()
};
}
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
}

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

@ -1,4 +1,4 @@
/** @import { Expression, Identifier, ImportDeclaration, MemberExpression, Program, Statement } from 'estree' */
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclarator } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '#compiler/builders';
@ -160,6 +160,50 @@ function transform_body(program, context) {
/** @type {Identifier | null} */
let last = null;
/**
* @param {Statement | VariableDeclarator | FunctionDeclaration | ClassDeclaration} node
*/
const push = (node) => {
const awaited = awaited_statements.get(node);
if (awaited) {
const ids = new Set();
const patterns = new Set();
for (const binding of awaited.metadata.dependencies) {
const dep = awaited_declarations.get(binding.node.name);
if (dep && dep.id !== awaited.id && !ids.has(dep.id)) {
ids.add(dep.id);
patterns.add(dep.pattern);
}
}
if (last) {
ids.add(last);
}
const rhs =
node.type === 'VariableDeclarator'
? node.init ?? b.block([])
: node.type === 'ExpressionStatement'
? node.expression
: b.block([node]);
out.push(
b.var(
awaited.id,
b.call('$.run', b.array([...ids]), b.arrow([...patterns], rhs, awaited.has_await))
)
);
last = awaited.id;
} else if (node.type === 'VariableDeclarator') {
out.push(b.var(node.id, node.init));
} else {
out.push(node);
}
};
for (let node of program.body) {
if (node.type === 'ImportDeclaration') {
// TODO we can get rid of the visitor
@ -183,67 +227,13 @@ function transform_body(program, context) {
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
const awaited = awaited_statements.get(declarator);
if (awaited) {
// TODO dependencies
out.push(
b.var(
awaited.id,
b.call(
'$.run',
b.array([]),
b.arrow([], declarator.init ?? b.block([]), awaited.has_await)
)
)
);
last = awaited.id;
} else {
out.push(b.var(declarator.id, declarator.init));
}
push(declarator);
}
} else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') {
// TODO
push(node);
} else {
const awaited = awaited_statements.get(node);
if (awaited) {
const ids = new Set();
const patterns = new Set();
for (const binding of awaited.metadata.dependencies) {
const dep = awaited_declarations.get(binding.node.name);
if (dep && !ids.has(dep.id)) {
ids.add(dep.id);
patterns.add(dep.pattern);
}
}
if (last) {
ids.add(last);
}
// TODO dependencies
out.push(
b.var(
awaited.id,
b.call(
'$.run',
b.array([...ids]),
b.arrow(
[...patterns],
node.type === 'ExpressionStatement' ? node.expression : b.block([node]),
awaited.has_await
)
)
)
);
last = awaited.id;
} else {
out.push(node);
}
push(node);
}
}

@ -31,6 +31,20 @@ export interface ReactiveStatement {
dependencies: Binding[];
}
export interface AwaitedDeclaration {
id: Identifier;
has_await: boolean;
pattern: Pattern;
metadata: ExpressionMetadata;
updated_by: Set<Identifier>;
}
export interface AwaitedStatement {
id: Identifier;
has_await: boolean;
metadata: ExpressionMetadata;
}
/**
* Analysis common to modules and components
*/
@ -125,22 +139,10 @@ export interface ComponentAnalysis extends Analysis {
*
* ...and references to `a` or `b` in the template should be mediated by `$$0` and `$$1`
*/
awaited_declarations: Map<
string,
{
id: Identifier;
has_await: boolean;
pattern: Pattern;
metadata: ExpressionMetadata;
updated_by: Set<Identifier>;
}
>;
awaited_declarations: Map<string, AwaitedDeclaration>;
/**
* Information about top-level instance statements that need to be transformed
* so that we can run the template synchronously
*/
awaited_statements: Map<
Statement | ModuleDeclaration | VariableDeclarator,
{ id: Identifier; has_await: boolean; metadata: ExpressionMetadata }
>;
awaited_statements: Map<Statement | ModuleDeclaration | VariableDeclarator, AwaitedStatement>;
}

@ -17,6 +17,7 @@ import type {
} from 'estree';
import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
import type { AwaitedDeclaration } from '../phases/types';
/**
* - `html` the default, for e.g. `<div>` or `<span>`
@ -465,6 +466,9 @@ export namespace AST {
alternate: Fragment | null;
/** @internal */
metadata: {
async: {
declarations: Set<AwaitedDeclaration>;
};
expression: ExpressionMetadata;
};
}

@ -14,10 +14,11 @@ import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<Promise<any>>} dependencies
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
export function async(node, dependencies, expressions, fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
@ -35,7 +36,7 @@ export function async(node, expressions, fn) {
set_hydrate_node(end);
}
flatten([], expressions, (values) => {
flatten(dependencies, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);

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

@ -36,12 +36,12 @@ import {
import { create_text } from '../dom/operations.js';
/**
*
* @param {Array<Promise<any>>} dependencies
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
*/
export function flatten(sync, async, fn) {
export function flatten(dependencies, sync, async, fn) {
const d = is_runes() ? derived : derived_safe_equal;
if (async.length === 0) {
@ -56,29 +56,39 @@ export function flatten(sync, async, fn) {
var was_hydrating = hydrating;
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
restore();
Promise.all(dependencies).then((values) => {
restore();
try {
fn([...sync.map(d), ...result]);
} catch (error) {
// ignore errors in blocks that have already been destroyed
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
const result = Promise.all(
async.map((expression) => async_derived(() => expression(...values)))
)
.then((result) => {
restore();
try {
fn([...sync.map(d), ...result]);
} catch (error) {
// ignore errors in blocks that have already been destroyed
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
}
}
if (was_hydrating) {
set_hydrating(false);
}
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate();
unset_context();
})
.catch((error) => {
invoke_error_boundary(error, parent);
});
batch?.deactivate();
unset_context();
})
.catch((error) => {
invoke_error_boundary(error, parent);
});
unset_context();
return result;
});
}
/**
@ -259,3 +269,12 @@ export async function async_body(anchor, fn) {
unset_context();
}
}
/**
* @param {Array<Promise<any>>} deps
* @param {(...deps: any) => any} fn
*/
export function run(deps, fn) {
// TODO save/restore context
return Promise.all(deps).then(fn);
}

Loading…
Cancel
Save