Rich Harris 8 months ago
parent 02c2ca4843
commit c73de77412

@ -7,9 +7,27 @@
*/
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
const blocking = tla || !!context.state.expression;
let suspend = tla;
if (blocking) {
if (context.state.expression) {
// wrap the expression in `(await $.suspend(...)).exit()` if necessary,
// i.e. whether anything could potentially be read _after_ the await
let i = context.path.length;
while (i--) {
const parent = context.path[i];
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata?.expression === context.state.expression) {
break;
}
// TODO make this more accurate — we don't need to call suspend
// if this is the last thing that could be read
suspend = true;
}
}
if (suspend) {
if (!context.state.analysis.runes) {
throw new Error('TODO runes mode only');
}

@ -53,7 +53,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Expressions used inside the render effect */
readonly expressions: Expression[];
readonly expressions: Array<{ id: Identifier; expression: Expression; is_async: boolean }>;
/** The HTML template string */
readonly template: Array<string | Expression>;
readonly locations: SourceLocation[];

@ -364,7 +364,11 @@ export function RegularElement(node, context) {
// (e.g. `<span>{location}</span>`), set `textContent` programmatically
const use_text_content =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
trimmed.every(
(node) =>
node.type === 'Text' ||
(!node.metadata.expression.has_state && !node.metadata.expression.is_async)
) &&
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
@ -537,8 +541,8 @@ function build_element_attribute_update_assignment(
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
let { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
get_expression_id(state, value)
let { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) =>
get_expression_id(state, value, is_async)
);
if (name === 'autofocus') {
@ -665,8 +669,8 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
get_expression_id(state, value)
const { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) =>
get_expression_id(state, value, is_async)
);
const inner_assignment = b.assignment(

@ -35,8 +35,10 @@ export function build_set_attributes(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
get_expression_id(context.state, value)
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, is_async) => get_expression_id(context.state, value, is_async)
);
if (
@ -111,8 +113,8 @@ export function build_style_directives(
let value =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value) =>
get_expression_id(context.state, value)
: build_attribute_value(directive.value, context, (value, is_async) =>
get_expression_id(context.state, value, is_async)
).value;
const update = b.stmt(
@ -149,11 +151,11 @@ export function build_class_directives(
) {
const state = context.state;
for (const directive of class_directives) {
const { has_state, has_call } = directive.metadata.expression;
const { has_state, has_call, is_async } = directive.metadata.expression;
let value = /** @type {Expression} */ (context.visit(directive.expression));
if (has_call) {
value = get_expression_id(state, value);
if (has_call || is_async) {
value = get_expression_id(state, value, is_async);
}
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
@ -169,7 +171,7 @@ export function build_class_directives(
/**
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
* @param {(value: Expression) => Expression} memoize
* @param {(value: Expression, is_async: boolean) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_attribute_value(value, context, memoize = (value) => value) {
@ -187,7 +189,10 @@ export function build_attribute_value(value, context, memoize = (value) => value
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
return {
value: chunk.metadata.expression.has_call ? memoize(expression) : expression,
value:
chunk.metadata.expression.has_call || chunk.metadata.expression.is_async
? memoize(expression, chunk.metadata.expression.is_async)
: expression,
has_state: chunk.metadata.expression.has_state
};
}

@ -23,16 +23,20 @@ export function memoize_expression(state, value) {
/**
*
* @param {ComponentClientTransformState} state
* @param {Expression} value
* @param {Expression} expression
* @param {boolean} is_async
*/
export function get_expression_id(state, value) {
export function get_expression_id(state, expression, is_async) {
for (let i = 0; i < state.expressions.length; i += 1) {
if (compare_expressions(state.expressions[i], value)) {
return b.id(`$${i}`);
if (compare_expressions(state.expressions[i].expression, expression)) {
return state.expressions[i].id;
}
}
return b.id(`$${state.expressions.push(value) - 1}`);
const id = b.id(''); // filled in later
state.expressions.push({ id, expression, is_async });
return id;
}
/**
@ -79,14 +83,14 @@ function compare_expressions(a, b) {
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @param {(value: Expression) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
* @param {(value: Expression, is_async: boolean) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean, is_async: boolean }}
*/
export function build_template_chunk(
values,
visit,
state,
memoize = (value) => get_expression_id(state, value)
memoize = (value, is_async) => get_expression_id(state, value, is_async)
) {
/** @type {Expression[]} */
const expressions = [];
@ -95,6 +99,7 @@ export function build_template_chunk(
const quasis = [quasi];
let has_state = false;
let is_async = false;
for (let i = 0; i < values.length; i++) {
const node = values[i];
@ -108,16 +113,17 @@ export function build_template_chunk(
} else {
let value = /** @type {Expression} */ (visit(node.expression, state));
has_state ||= node.metadata.expression.has_state;
is_async ||= node.metadata.expression.is_async;
has_state ||= is_async || node.metadata.expression.has_state;
if (node.metadata.expression.has_call) {
value = memoize(value);
if (node.metadata.expression.has_call || node.metadata.expression.is_async) {
value = memoize(value, node.metadata.expression.is_async);
}
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value, has_state };
return { value, has_state, is_async };
} else {
let expression = value;
// only add nullish coallescence if it hasn't been added already
@ -148,25 +154,34 @@ export function build_template_chunk(
const value = b.template(quasis, expressions);
return { value, has_state };
return { value, has_state, is_async };
}
/**
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const sync = state.expressions.filter(({ is_async }) => !is_async);
const async = state.expressions.filter(({ is_async }) => is_async);
const all = [...sync, ...async];
for (let i = 0; i < all.length; i += 1) {
all[i].id.name = `$${i}`;
}
return b.stmt(
b.call(
'$.template_effect',
b.arrow(
state.expressions.map((_, i) => b.id(`$${i}`)),
all.map(({ id }) => id),
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
? state.update[0].expression
: b.block(state.update)
),
state.expressions.length > 0 &&
b.array(state.expressions.map((expression) => b.thunk(expression))),
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))),
async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))),
!state.analysis.runes && sync.length > 0 && b.id('$.derived_safe_equal')
)
);
}

@ -1,4 +1,4 @@
/** @import { Derived, Effect } from '#client' */
/** @import { Derived, Effect, Source } from '#client' */
import { DEV } from 'esm-env';
import {
CLEAN,
@ -80,10 +80,10 @@ export function derived(fn) {
/**
* @template V
* @param {() => Promise<V>} fn
* @returns {Promise<() => V>}
* @returns {Promise<Source<V>>}
*/
/*#__NO_SIDE_EFFECTS__*/
export async function async_derived(fn) {
export function async_derived(fn) {
if (!active_effect) {
throw new Error('TODO cannot create unowned async derived');
}
@ -103,10 +103,7 @@ export async function async_derived(fn) {
// TODO what happens when the promise rejects?
});
// wait for the initial promise
(await suspend(promise)).exit();
return () => get(value);
return promise.then(() => value);
}
/**

@ -1,4 +1,4 @@
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager, Value } from '#client' */
import {
check_dirtiness,
component_context,
@ -44,7 +44,8 @@ import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived, destroy_derived } from './deriveds.js';
import { async_derived, derived, destroy_derived } from './deriveds.js';
import { suspend } from '../dom/blocks/boundary.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -345,11 +346,18 @@ export function render_effect(fn) {
/**
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} thunks
* @returns {Effect}
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
*/
export function template_effect(fn, thunks = [], d = derived) {
const deriveds = thunks.map(d);
export async function template_effect(fn, sync = [], async = [], d = derived) {
/** @type {Value[]} */
const deriveds = sync.map(d);
if (async.length > 0) {
const async_deriveds = (await suspend(Promise.all(async.map(async_derived)))).exit();
deriveds.push(...async_deriveds);
}
const effect = () => fn(...deriveds.map(get));
if (DEV) {
@ -358,7 +366,7 @@ export function template_effect(fn, thunks = [], d = derived) {
});
}
return block(effect);
block(effect);
}
/**

@ -19,6 +19,8 @@ export default test({
async test({ assert, target }) {
d.resolve('hello');
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await tick();
assert.htmlEqual(target.innerHTML, '<p>hello</p>');
}

Loading…
Cancel
Save