Skip to content

Commit aa9a72c

Browse files
better errors for sync-in-async
1 parent f1c0634 commit aa9a72c

File tree

11 files changed

+111
-91
lines changed

11 files changed

+111
-91
lines changed

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { TitleElement } from './visitors/TitleElement.js';
4040
import { UpdateExpression } from './visitors/UpdateExpression.js';
4141
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
4242
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
43+
import { call_child_payload } from './visitors/shared/utils.js';
4344

4445
/** @type {Visitors} */
4546
const global_visitors = {
@@ -238,18 +239,12 @@ export function server_component(analysis, options) {
238239
}
239240

240241
const component_block = b.block([
241-
b.stmt(
242-
b.call(
243-
'$$payload.child',
244-
b.arrow(
245-
[b.id('$$payload')],
246-
b.block([
247-
.../** @type {Statement[]} */ (instance.body),
248-
.../** @type {Statement[]} */ (template.body)
249-
]),
250-
analysis.suspends_without_fallback
251-
)
252-
)
242+
call_child_payload(
243+
b.block([
244+
.../** @type {Statement[]} */ (instance.body),
245+
.../** @type {Statement[]} */ (template.body)
246+
]),
247+
analysis.suspends_without_fallback
253248
)
254249
]);
255250

packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
33
import { clean_nodes, infer_namespace } from '../../utils.js';
44
import * as b from '#compiler/builders';
5-
import { empty_comment, process_children, build_template } from './shared/utils.js';
5+
import {
6+
empty_comment,
7+
process_children,
8+
build_template,
9+
call_child_payload
10+
} from './shared/utils.js';
611

712
/**
813
* @param {AST.Fragment} node
@@ -49,12 +54,7 @@ export function Fragment(node, context) {
4954
b.array(node.metadata.hoisted_promises.promises)
5055
),
5156
...state.init,
52-
b.stmt(
53-
b.call(
54-
'$$payload.child',
55-
b.arrow([b.id('$$payload')], b.block(build_template(state.template)), true)
56-
)
57-
)
57+
call_child_payload(b.block(build_template(state.template)), true)
5858
]);
5959
}
6060

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
process_children,
1313
build_template,
1414
build_attribute_value,
15-
wrap_in_child_payload
15+
call_child_payload
1616
} from './shared/utils.js';
1717

1818
/**
@@ -203,7 +203,7 @@ export function RegularElement(node, context) {
203203
// TODO is this cast safe?
204204
const elements = state.template.splice(template_start, Infinity);
205205
state.template.push(
206-
wrap_in_child_payload(
206+
call_child_payload(
207207
b.block(build_template(elements)),
208208
// TODO this will always produce correct results (because it will produce an async function if the surrounding component is async)
209209
// but it will false-positive and create unnecessary async functions (eg. when the component is async but the select element is not)

packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ export function SvelteBoundary(node, context) {
2828
);
2929
}
3030
} else {
31-
// No pending snippet - render main content (may be async or sync)
32-
context.state.template.push(b.literal(BLOCK_OPEN)); // <!--[-->
31+
context.state.template.push(b.literal(BLOCK_OPEN));
3332
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
3433
}
3534

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { AST } from '#compiler' */
22
/** @import { ComponentContext } from '../types.js' */
33
import * as b from '#compiler/builders';
4-
import { process_children, build_template } from './shared/utils.js';
4+
import { process_children, build_template, call_child_payload } from './shared/utils.js';
55

66
/**
77
* @param {AST.TitleElement} node
@@ -14,29 +14,20 @@ export function TitleElement(node, context) {
1414
template.push(b.literal('</title>'));
1515

1616
context.state.init.push(
17-
b.stmt(
18-
b.call(
19-
'$$payload.child',
20-
// this nonsense is necessary so that the write to the title is as tightly scoped to a specific location
21-
// in the async tree as possible. This lets us use `get_path` to compare this assignment to other assignments
22-
// so that we can overwrite earlier assignments with later ones.
23-
b.arrow(
24-
[b.id('$$payload')],
25-
b.block([
26-
b.const('path', b.call('$$payload.get_path')),
27-
b.let('title'),
28-
...build_template(template, b.id('title'), '='),
29-
b.stmt(
30-
b.assignment(
31-
'=',
32-
b.id('$$payload.global.head.title'),
33-
b.object([b.init('path', b.id('path')), b.init('value', b.id('title'))])
34-
)
35-
)
36-
]),
37-
node.metadata.has_await
17+
call_child_payload(
18+
b.block([
19+
b.const('path', b.call('$$payload.get_path')),
20+
b.let('title'),
21+
...build_template(template, b.id('title'), '='),
22+
b.stmt(
23+
b.assignment(
24+
'=',
25+
b.id('$$payload.global.head.title'),
26+
b.object([b.init('path', b.id('path')), b.init('value', b.id('title'))])
27+
)
3828
)
39-
)
29+
]),
30+
node.metadata.has_await
4031
)
4132
);
4233
}

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../../types.js' */
4-
import { empty_comment, build_attribute_value } from './utils.js';
4+
import { empty_comment, build_attribute_value, call_child_payload } from './utils.js';
55
import * as b from '#compiler/builders';
66
import { is_element_node } from '../../../../nodes.js';
77
import { dev } from '../../../../../state.js';
@@ -234,16 +234,7 @@ export function build_inline_component(node, expression, context) {
234234
// not necessary -- eg. when the component is asynchronous but the child content is not.
235235
// May or may not be worth optimizing.
236236
b.block([
237-
b.stmt(
238-
b.call(
239-
'$$payload.child',
240-
b.arrow(
241-
[b.id('$$payload')],
242-
b.block(block.body),
243-
context.state.analysis.suspends_without_fallback
244-
)
245-
)
246-
)
237+
call_child_payload(b.block(block.body), context.state.analysis.suspends_without_fallback)
247238
])
248239
);
249240

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,6 @@ export function build_getter(node, state) {
263263
* @param {boolean} async
264264
* @returns {Statement}
265265
*/
266-
export function wrap_in_child_payload(body, async) {
266+
export function call_child_payload(body, async) {
267267
return b.stmt(b.call('$$payload.child', b.arrow([b.id('$$payload')], body, async)));
268268
}

packages/svelte/src/compiler/phases/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export interface ComponentAnalysis extends Analysis {
107107
* Every snippet that is declared locally
108108
*/
109109
snippets: Set<AST.SnippetBlock>;
110-
/** Whether the component uses `await` in a context that would require an `await` on the server. */
110+
/** Whether the component uses `await` in a context that causes suspense outside of any boundary with a pending snippet. */
111111
suspends_without_fallback: boolean;
112112
hoisted_promises: Map<Expression, MemberExpression>;
113113
}

packages/svelte/src/internal/client/render.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ import { DEV } from 'esm-env';
44
import {
55
clear_text_content,
66
create_text,
7-
first_child,
87
get_first_child,
98
get_next_sibling,
109
init_operations
1110
} from './dom/operations.js';
1211
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
1312
import { active_effect } from './runtime.js';
1413
import { push, pop, component_context } from './context.js';
15-
import { component_root, branch } from './reactivity/effects.js';
14+
import { component_root } from './reactivity/effects.js';
1615
import {
1716
hydrate_next,
1817
hydrate_node,
@@ -29,7 +28,7 @@ import {
2928
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
3029
import * as w from './warnings.js';
3130
import * as e from './errors.js';
32-
import { append, assign_nodes, comment } from './dom/template.js';
31+
import { assign_nodes } from './dom/template.js';
3332
import { is_passive_event } from '../../utils.js';
3433
import { COMMENT_NODE } from './constants.js';
3534
import { boundary } from './dom/blocks/boundary.js';

packages/svelte/src/internal/server/payload.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ export class Payload {
5555
local;
5656

5757
/**
58-
* @param {TreeState} [global]
58+
* @param {TreeState} global
5959
* @param {{ select_value: string | undefined }} [local]
6060
* @param {Payload | undefined} [parent]
6161
* @param {PayloadType} [type]
6262
*/
63-
constructor(global = new TreeState(), local = { select_value: undefined }, parent, type) {
63+
constructor(global, local = { select_value: undefined }, parent, type) {
6464
this.global = global;
6565
this.local = { ...local };
6666
this.parent = parent;
@@ -79,6 +79,12 @@ export class Payload {
7979
this.#out.push(child);
8080
const result = render(child);
8181
if (result instanceof Promise) {
82+
if (this.global.mode === 'sync') {
83+
// TODO more-proper error
84+
throw new Error('Encountered an asynchronous component while rendering synchronously');
85+
}
86+
// just to avoid unhandled promise rejections -- we'll end up throwing in `then` if something fails
87+
result.catch(() => {});
8288
child.promises.initial = result;
8389
}
8490
}
@@ -135,8 +141,11 @@ export class Payload {
135141
collect() {
136142
const content = Payload.#collect_content(this.#out, this.type);
137143
if (content instanceof Promise) {
138-
// TODO is there a good way to report where this is? Probably by using some sort of loc or stack trace in `child` creation.
139-
throw new Error('Encountered an asynchronous component while rendering synchronously');
144+
// TODO improve message
145+
// guess you could also end up here if you called `collect` in an async context but... just don't bro
146+
throw new Error(
147+
'invariant: should never reach this, as child throws when it encounters async work in a synchronous context'
148+
);
140149
}
141150

142151
return content;
@@ -153,6 +162,11 @@ export class Payload {
153162
* @param {Payload} other
154163
*/
155164
subsume(other) {
165+
if (this.global.mode !== other.global.mode) {
166+
// TODO message - this should be impossible though
167+
throw new Error('invariant: a payload cannot switch modes');
168+
}
169+
156170
this.global.subsume(other.global);
157171
this.local = other.local;
158172
this.#out = other.#out.map((item) => {
@@ -287,6 +301,9 @@ export class TreeState {
287301
/** @type {TreeHeadState} */
288302
#head;
289303

304+
/** @type {'sync' | 'async'} */
305+
#mode;
306+
290307
get css() {
291308
return this.#css;
292309
}
@@ -299,17 +316,23 @@ export class TreeState {
299316
return this.#head;
300317
}
301318

319+
get mode() {
320+
return this.#mode;
321+
}
322+
302323
/**
324+
* @param {'sync' | 'async'} mode
303325
* @param {string} [id_prefix]
304326
*/
305-
constructor(id_prefix = '') {
327+
constructor(mode, id_prefix = '') {
306328
this.#uid = props_id_generator(id_prefix);
307329
this.#css = new Set();
308330
this.#head = new TreeHeadState(this.#uid);
331+
this.#mode = mode;
309332
}
310333

311334
copy() {
312-
const state = new TreeState();
335+
const state = new TreeState(this.#mode);
313336
state.#css = new Set(this.#css);
314337
state.#head = this.#head.copy();
315338
state.#uid = this.#uid;
@@ -323,6 +346,7 @@ export class TreeState {
323346
this.#css = other.#css;
324347
this.#uid = other.#uid;
325348
this.#head.subsume(other.#head);
349+
this.#mode = other.#mode;
326350
}
327351
}
328352

0 commit comments

Comments
 (0)