Skip to content

Commit c73de77

Browse files
committed
fix
1 parent 02c2ca4 commit c73de77

File tree

8 files changed

+97
-48
lines changed

8 files changed

+97
-48
lines changed

packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,27 @@
77
*/
88
export function AwaitExpression(node, context) {
99
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
10-
const blocking = tla || !!context.state.expression;
10+
let suspend = tla;
1111

12-
if (blocking) {
12+
if (context.state.expression) {
13+
// wrap the expression in `(await $.suspend(...)).exit()` if necessary,
14+
// i.e. whether anything could potentially be read _after_ the await
15+
let i = context.path.length;
16+
while (i--) {
17+
const parent = context.path[i];
18+
19+
// @ts-expect-error we could probably use a neater/more robust mechanism
20+
if (parent.metadata?.expression === context.state.expression) {
21+
break;
22+
}
23+
24+
// TODO make this more accurate — we don't need to call suspend
25+
// if this is the last thing that could be read
26+
suspend = true;
27+
}
28+
}
29+
30+
if (suspend) {
1331
if (!context.state.analysis.runes) {
1432
throw new Error('TODO runes mode only');
1533
}

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

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

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,11 @@ export function RegularElement(node, context) {
364364
// (e.g. `<span>{location}</span>`), set `textContent` programmatically
365365
const use_text_content =
366366
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
367-
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
367+
trimmed.every(
368+
(node) =>
369+
node.type === 'Text' ||
370+
(!node.metadata.expression.has_state && !node.metadata.expression.is_async)
371+
) &&
368372
trimmed.some((node) => node.type === 'ExpressionTag');
369373

370374
if (use_text_content) {
@@ -537,8 +541,8 @@ function build_element_attribute_update_assignment(
537541
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
538542
const is_mathml = context.state.metadata.namespace === 'mathml';
539543

540-
let { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
541-
get_expression_id(state, value)
544+
let { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) =>
545+
get_expression_id(state, value, is_async)
542546
);
543547

544548
if (name === 'autofocus') {
@@ -665,8 +669,8 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
665669
*/
666670
function build_element_special_value_attribute(element, node_id, attribute, context) {
667671
const state = context.state;
668-
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
669-
get_expression_id(state, value)
672+
const { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) =>
673+
get_expression_id(state, value, is_async)
670674
);
671675

672676
const inner_assignment = b.assignment(

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js

+14-9
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ export function build_set_attributes(
3535

3636
for (const attribute of attributes) {
3737
if (attribute.type === 'Attribute') {
38-
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
39-
get_expression_id(context.state, value)
38+
const { value, has_state } = build_attribute_value(
39+
attribute.value,
40+
context,
41+
(value, is_async) => get_expression_id(context.state, value, is_async)
4042
);
4143

4244
if (
@@ -111,8 +113,8 @@ export function build_style_directives(
111113
let value =
112114
directive.value === true
113115
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
114-
: build_attribute_value(directive.value, context, (value) =>
115-
get_expression_id(context.state, value)
116+
: build_attribute_value(directive.value, context, (value, is_async) =>
117+
get_expression_id(context.state, value, is_async)
116118
).value;
117119

118120
const update = b.stmt(
@@ -149,11 +151,11 @@ export function build_class_directives(
149151
) {
150152
const state = context.state;
151153
for (const directive of class_directives) {
152-
const { has_state, has_call } = directive.metadata.expression;
154+
const { has_state, has_call, is_async } = directive.metadata.expression;
153155
let value = /** @type {Expression} */ (context.visit(directive.expression));
154156

155-
if (has_call) {
156-
value = get_expression_id(state, value);
157+
if (has_call || is_async) {
158+
value = get_expression_id(state, value, is_async);
157159
}
158160

159161
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
@@ -169,7 +171,7 @@ export function build_class_directives(
169171
/**
170172
* @param {AST.Attribute['value']} value
171173
* @param {ComponentContext} context
172-
* @param {(value: Expression) => Expression} memoize
174+
* @param {(value: Expression, is_async: boolean) => Expression} memoize
173175
* @returns {{ value: Expression, has_state: boolean }}
174176
*/
175177
export function build_attribute_value(value, context, memoize = (value) => value) {
@@ -187,7 +189,10 @@ export function build_attribute_value(value, context, memoize = (value) => value
187189
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
188190

189191
return {
190-
value: chunk.metadata.expression.has_call ? memoize(expression) : expression,
192+
value:
193+
chunk.metadata.expression.has_call || chunk.metadata.expression.is_async
194+
? memoize(expression, chunk.metadata.expression.is_async)
195+
: expression,
191196
has_state: chunk.metadata.expression.has_state
192197
};
193198
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

+32-17
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,20 @@ export function memoize_expression(state, value) {
2323
/**
2424
*
2525
* @param {ComponentClientTransformState} state
26-
* @param {Expression} value
26+
* @param {Expression} expression
27+
* @param {boolean} is_async
2728
*/
28-
export function get_expression_id(state, value) {
29+
export function get_expression_id(state, expression, is_async) {
2930
for (let i = 0; i < state.expressions.length; i += 1) {
30-
if (compare_expressions(state.expressions[i], value)) {
31-
return b.id(`$${i}`);
31+
if (compare_expressions(state.expressions[i].expression, expression)) {
32+
return state.expressions[i].id;
3233
}
3334
}
3435

35-
return b.id(`$${state.expressions.push(value) - 1}`);
36+
const id = b.id(''); // filled in later
37+
state.expressions.push({ id, expression, is_async });
38+
39+
return id;
3640
}
3741

3842
/**
@@ -79,14 +83,14 @@ function compare_expressions(a, b) {
7983
* @param {Array<AST.Text | AST.ExpressionTag>} values
8084
* @param {(node: AST.SvelteNode, state: any) => any} visit
8185
* @param {ComponentClientTransformState} state
82-
* @param {(value: Expression) => Expression} memoize
83-
* @returns {{ value: Expression, has_state: boolean }}
86+
* @param {(value: Expression, is_async: boolean) => Expression} memoize
87+
* @returns {{ value: Expression, has_state: boolean, is_async: boolean }}
8488
*/
8589
export function build_template_chunk(
8690
values,
8791
visit,
8892
state,
89-
memoize = (value) => get_expression_id(state, value)
93+
memoize = (value, is_async) => get_expression_id(state, value, is_async)
9094
) {
9195
/** @type {Expression[]} */
9296
const expressions = [];
@@ -95,6 +99,7 @@ export function build_template_chunk(
9599
const quasis = [quasi];
96100

97101
let has_state = false;
102+
let is_async = false;
98103

99104
for (let i = 0; i < values.length; i++) {
100105
const node = values[i];
@@ -108,16 +113,17 @@ export function build_template_chunk(
108113
} else {
109114
let value = /** @type {Expression} */ (visit(node.expression, state));
110115

111-
has_state ||= node.metadata.expression.has_state;
116+
is_async ||= node.metadata.expression.is_async;
117+
has_state ||= is_async || node.metadata.expression.has_state;
112118

113-
if (node.metadata.expression.has_call) {
114-
value = memoize(value);
119+
if (node.metadata.expression.has_call || node.metadata.expression.is_async) {
120+
value = memoize(value, node.metadata.expression.is_async);
115121
}
116122

117123
if (values.length === 1) {
118124
// If we have a single expression, then pass that in directly to possibly avoid doing
119125
// extra work in the template_effect (instead we do the work in set_text).
120-
return { value, has_state };
126+
return { value, has_state, is_async };
121127
} else {
122128
let expression = value;
123129
// only add nullish coallescence if it hasn't been added already
@@ -148,25 +154,34 @@ export function build_template_chunk(
148154

149155
const value = b.template(quasis, expressions);
150156

151-
return { value, has_state };
157+
return { value, has_state, is_async };
152158
}
153159

154160
/**
155161
* @param {ComponentClientTransformState} state
156162
*/
157163
export function build_render_statement(state) {
164+
const sync = state.expressions.filter(({ is_async }) => !is_async);
165+
const async = state.expressions.filter(({ is_async }) => is_async);
166+
167+
const all = [...sync, ...async];
168+
169+
for (let i = 0; i < all.length; i += 1) {
170+
all[i].id.name = `$${i}`;
171+
}
172+
158173
return b.stmt(
159174
b.call(
160175
'$.template_effect',
161176
b.arrow(
162-
state.expressions.map((_, i) => b.id(`$${i}`)),
177+
all.map(({ id }) => id),
163178
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
164179
? state.update[0].expression
165180
: b.block(state.update)
166181
),
167-
state.expressions.length > 0 &&
168-
b.array(state.expressions.map((expression) => b.thunk(expression))),
169-
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
182+
all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))),
183+
async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))),
184+
!state.analysis.runes && sync.length > 0 && b.id('$.derived_safe_equal')
170185
)
171186
);
172187
}

packages/svelte/src/internal/client/reactivity/deriveds.js

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Derived, Effect } from '#client' */
1+
/** @import { Derived, Effect, Source } from '#client' */
22
import { DEV } from 'esm-env';
33
import {
44
CLEAN,
@@ -80,10 +80,10 @@ export function derived(fn) {
8080
/**
8181
* @template V
8282
* @param {() => Promise<V>} fn
83-
* @returns {Promise<() => V>}
83+
* @returns {Promise<Source<V>>}
8484
*/
8585
/*#__NO_SIDE_EFFECTS__*/
86-
export async function async_derived(fn) {
86+
export function async_derived(fn) {
8787
if (!active_effect) {
8888
throw new Error('TODO cannot create unowned async derived');
8989
}
@@ -103,10 +103,7 @@ export async function async_derived(fn) {
103103
// TODO what happens when the promise rejects?
104104
});
105105

106-
// wait for the initial promise
107-
(await suspend(promise)).exit();
108-
109-
return () => get(value);
106+
return promise.then(() => value);
110107
}
111108

112109
/**

packages/svelte/src/internal/client/reactivity/effects.js

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
1+
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager, Value } from '#client' */
22
import {
33
check_dirtiness,
44
component_context,
@@ -44,7 +44,8 @@ import * as e from '../errors.js';
4444
import { DEV } from 'esm-env';
4545
import { define_property } from '../../shared/utils.js';
4646
import { get_next_sibling } from '../dom/operations.js';
47-
import { derived, destroy_derived } from './deriveds.js';
47+
import { async_derived, derived, destroy_derived } from './deriveds.js';
48+
import { suspend } from '../dom/blocks/boundary.js';
4849

4950
/**
5051
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -345,11 +346,18 @@ export function render_effect(fn) {
345346

346347
/**
347348
* @param {(...expressions: any) => void | (() => void)} fn
348-
* @param {Array<() => any>} thunks
349-
* @returns {Effect}
349+
* @param {Array<() => any>} sync
350+
* @param {Array<() => Promise<any>>} async
350351
*/
351-
export function template_effect(fn, thunks = [], d = derived) {
352-
const deriveds = thunks.map(d);
352+
export async function template_effect(fn, sync = [], async = [], d = derived) {
353+
/** @type {Value[]} */
354+
const deriveds = sync.map(d);
355+
356+
if (async.length > 0) {
357+
const async_deriveds = (await suspend(Promise.all(async.map(async_derived)))).exit();
358+
deriveds.push(...async_deriveds);
359+
}
360+
353361
const effect = () => fn(...deriveds.map(get));
354362

355363
if (DEV) {
@@ -358,7 +366,7 @@ export function template_effect(fn, thunks = [], d = derived) {
358366
});
359367
}
360368

361-
return block(effect);
369+
block(effect);
362370
}
363371

364372
/**

packages/svelte/tests/runtime-runes/samples/async-expression/_config.js

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export default test({
1919
async test({ assert, target }) {
2020
d.resolve('hello');
2121
await Promise.resolve();
22+
await Promise.resolve();
23+
await Promise.resolve();
2224
await tick();
2325
assert.htmlEqual(target.innerHTML, '<p>hello</p>');
2426
}

0 commit comments

Comments
 (0)