Skip to content

Conversation

elliott-with-the-longest-name-on-github
Copy link
Contributor

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented Sep 10, 2025

⚠️ No Changeset found

Latest commit: 894b93e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@svelte-docs-bot
Copy link

```

The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Feels like this error should go away in favor of an internal invariant. Because we wrap your app in a root boundary it should never be possible to encounter.

@@ -177,7 +177,8 @@ export default function element(parser) {
mathml: false,
scoped: false,
has_spread: false,
path: []
path: [],
synthetic_value_node: null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of a fix to options

@@ -11,7 +11,9 @@ export function create_fragment(transparent = false) {
metadata: {
transparent,
dynamic: false,
has_await: false
has_await: false,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were forgetting to initialize this previously, which luckily wasn't a problem because undefined is falsey

has_await: false
has_await: false,
// name is added later, after we've done scope analysis
hoisted_promises: { name: '', promises: [] }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to see a better solution for this (see below in `2-analyze/index.js)

@@ -180,6 +169,54 @@ export class Boundary {
}
}

#detect_server_state() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO this is a weak name

* @returns {Node | null}
*/
export function first_child(fragment, is_text) {
export function first_child(fragment, is_text = false) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was being called without a second argument in compiled code so I updated it just to make it more correct

var ctx = /** @type {ComponentContext} */ (component_context);
ctx.c = context;
}
boundary(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for reviewers: in reality this is very similar to what we had before with branch, as children is just stuck into a branch inside of boundary. The only real difference is that the branch is now inside of a block.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Rich pointed out, Payload is kind of a weird name now that this is a Big Thing with logic complicatedness and stuff. Should it be called ContentTree or something now?

@@ -31,8 +31,21 @@ export function asClassComponent(component) {
html: result.body
};
};
/** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map<any, any>; }) => Promise<{ html: any; css: { code: string; map: any; }; head: string; }> } */

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsure if we should do this -- I needed it to be able to test with minimal changes to Kit but if we're dropping svelte 4 in the next major it could be worth just not supporting async rendering for class components. that being said it's a tiny lift.

Comment on lines 20 to 32
if (context.state.async_hoist_boundary) {
const len = context.state.async_hoist_boundary.metadata.hoisted_promises.promises.push(
node.argument
);
context.state.analysis.hoisted_promises.set(
node.argument,
b.member(
b.id(context.state.async_hoist_boundary.metadata.hoisted_promises.name),
b.literal(len - 1),
true
)
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic isn't quite right — if you have something like this...

{await (await duplicated())}

...you get this output:

$$payload.child(async ($$payload) => {
	const promises = [await duplicated(), duplicated()];

	$$payload.child(async ($$payload) => {
		$$payload.push(`<!---->${$.escape(await promises[0])}`);
	});
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(semi-related: it'd be nice if we skipped the indirection in the common case that there's only a single promise)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the latter issue is resolved on remove-async-hoist-boundary

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you know what? I think this is the wrong place to think about hoisting. It means that in a case like this...

{false && await foo()}

...foo() will be called, incorrectly. Or even worse:

{object && await object.promise}

I suspect we need to use the same de-waterfalling mechanism that's used for client code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(well not the same, since Memoizer is concerned with... memoizing, but you get the idea)

const promises = [Promise.resolve([first, second, third])];
const each_array = $.ensure_array_like(await promises[0]);

$$payload.child(async ($$payload) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this an async function? Nothing is awaited inside it. I'd expect either this to be a sync function, or to see this...

const each_array = $.ensure_array_like(await Promise.resolve([first, second, third]));

...inside this call (in which case the topmost $$payload.child(...) would be passed a sync function)

Comment on lines +6 to +8
const { promise: main_promise, resolve: main_resolve } = Promise.withResolvers();
const { promise: a_promise, resolve: a_resolve } = Promise.withResolvers();
const { promise: b_promise, resolve: b_resolve } = Promise.withResolvers();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor detail but it's way easier to do it like this :)

Suggested change
const { promise: main_promise, resolve: main_resolve } = Promise.withResolvers();
const { promise: a_promise, resolve: a_resolve } = Promise.withResolvers();
const { promise: b_promise, resolve: b_resolve } = Promise.withResolvers();
const main = Promise.withResolvers();
const a = Promise.withResolvers();
const b = Promise.withResolvers();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also noted, will clean up on test run thru


// regardless of resolution order, title should be the result of B, because it's the last-encountered
tick().then(() => {
main_resolve(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
main_resolve(true);
main.resolve(true);

tick().then(() => {
main_resolve(true);
tick().then(() => {
b_resolve(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
b_resolve(true);
b.resolve(true);

tick().then(() => {
b_resolve(true);
}).then(() => {
a_resolve(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
a_resolve(true);
a.resolve(true);

</script>

<svelte:head>
{#if await main_promise}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{#if await main_promise}
{#if await main.promise}

Comment on lines +26 to +27
<A promise={a_promise}/>
<B promise={b_promise}/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<A promise={a_promise}/>
<B promise={b_promise}/>
<A promise={a.promise}/>
<B promise={b.promise}/>

Comment on lines 134 to 136
if (node.type === 'Fragment') {
node.metadata.hoisted_promises.name = state.scope.generate('promises');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any particular reason this is here instead of the Fragment visitor?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because I felt like it 😂

no really it's here because I happened to be in this file looking at this function when I was thinking about it. Will move

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjusted this on the remove-async-hoist-boundary branch

Comment on lines 14 to 21
/**
* The "anchor" fragment for any hoisted promises. This is the root fragment when
* walking starts and until another boundary fragment is encountered, like a
* consequent or alternate of an `#if` or `#each` block. When this fragment is emitted
* during server transformation, the promise expressions will be hoisted out of the fragment
* and placed right above it in an array.
*/
async_hoist_boundary: AST.Fragment | null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unnecessary, we can just re-use fragment. See remove-async-hoist-boundary

* use fragment as async hoist boundary

* remove async_hoist_boundary

* only dewaterfall when necessary

* unused

* simplify/fix

* de-waterfall awaits in separate elements

* update snapshots

* remove unnecessary wrapper

* fix

* fix

* remove suspends_without_fallback

---------

Co-authored-by: Rich Harris <[email protected]>
Comment on lines +92 to +98
/**
* @param {(value: { head: string, body: string }) => void} onfulfilled
*/
async then(onfulfilled) {
const content = await Payload.#collect_content([this], this.type);
return onfulfilled(content);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this for? Find All References doesn't show anything — is there an await $$payload somewhere? could use a comment if so

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's awaited in render_async. This could be a method like collect_async too 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants