Skip to content

Commit 0672e48

Browse files
Rich-Harrisdummdidummelliott-with-the-longest-name-on-github
authored
feat: allow await in components (#15844)
* tidy * tidy * yes it can, apparently * tidy up * unused * complete merge * WIP * simplify * debugging help * WIP * unused * partial merge * WIP * fix * add test * rename * fix * unused * oops * chore: merge main into async branch (#16197) * chore: merge main into async branch * adjust test * fix: make effects depend on state created inside them (#16198) * make effects depend on state created inside them * fix, add github action * disable test in async mode * make batch.#deferred private * fix settled when awaits occur inside pending boundary * tweak * change behaviour of `tick()` to be requestAnimationFrame-based * get rid of a bunch of Promise.resolve chains * more * more * fix test * disallow `flushSync()` inside effects * regenerate * handle errors in block expressions * make validate_each_keys async-aware * for unowned deriveds, throw errors lazily * rename ASYNC_ERROR -> ERROR_VALUE, and avoid conflicts with other flags now that it's used with deriveds as well as sources * invoke boundary directly * local effect pending * update test * fix * fix * fix weird bug in tests * delete old changeset that somehow got left over here * Update .changeset/eleven-weeks-dance.md * update error details * unused * simplify * tweak * tweak * tweak * tweak * tidy up * handle errors in async block expressions * tweak * groundwork for async attribute_effect * dry out * fix async directives * tidy up * initialize option values before initing select values * simplify init_select * simplify * tweak * tidy up * tweak * on second thoughts just simplify it here * tidy * handle awaits in `<slot>` * unused * tidy up * tidy up * dry out * dry out * Revert "dry out" This reverts commit 2585516. * dry out * dry out * use let for block-scoped stuff * dry out * dry out * tidy up * only wrap awaits in `$.save` when necessary * oops * remove TODO comment (just checked) * oops, leftover * simplify * unused * remove logging * tweak * unused * unused * remove logging * partial fix * fix * remove unused EFFECT_HAS_DERIVED * Update packages/svelte/src/reactivity/create-subscriber.js Co-authored-by: Elliott Johnson <[email protected]> * Update packages/svelte/src/index-client.js Co-authored-by: Elliott Johnson <[email protected]> * Update packages/svelte/src/internal/client/runtime.js Co-authored-by: Elliott Johnson <[email protected]> * unused * Update packages/svelte/src/internal/client/reactivity/sources.js Co-authored-by: Elliott Johnson <[email protected]> * Update packages/svelte/src/internal/client/reactivity/deriveds.js Co-authored-by: Elliott Johnson <[email protected]> * Update packages/svelte/src/internal/client/reactivity/deriveds.js Co-authored-by: Elliott Johnson <[email protected]> * prettier * unused * fix flags * tweak * tweak * unused * fix * no idea what a 'boundary micro task' is or why it was deemed necessary but evidently it isn't * remove queue_boundary_micro_task * oops * note * tidy up * remove TODO * make method private * simplify * flesh out await_reactivity_loss warning * tweak * update test * fix * null out from_async_derived in more places * tidy up test * failing test * unused * fix test * fix * simplify. no idea what the async_mode_flag stuff is about, but it appears unnecessary * add async_derived_orphan error * regenerate * flesh out await_outside_boundary message * add some JSDoc * only update `$effect.pending()` if someone is listening, since it causes a double flush and makes debugging harder * tweak logic to make it clearer why and when we commit a batch * add a couple of comments * false -> 0 * add comment * unused * silence warning * add effect_pending_outside_reaction error * Update packages/svelte/src/compiler/types/index.d.ts Co-authored-by: Elliott Johnson <[email protected]> * suspend batch, not boundary * rename from_async_derived -> current_async_derived * tweak * remove TODO - this method is only called when pending snippet exists * use error boundary for test - vitest does some weird error swallowing afaict * flush less often * restore -> activate * remove TODO * move batch-related code into batch.js * make flush_queued_root_effects a method of batch * make process_effects a method of batch * make stuff private * unused * regenerate * update test * more JSDoc * add more JSDoc * branch and block effects do not also need to be render effects * tidy up * simplify * unused * move code where it belongs * remove, for now * fix * only apply error adjustments when error escapes boundaries * remove EFFECT_IS_UPDATING * is_dirty is a better name than check_dirtiness * duplicates are rare and harmless * apparently we no longer need the merging logic? we can simplify and fix stuff by removing it * tidy * don't commit stale batches * add skipped failing test * partial merge * WIP * WIP * WIP * tweak * tidy up * dont update derived status when time-travelling * tidy up * tidy up * tag async deriveds * tweak * bail out of secondary flushes * re-run blocks on subsequent flushes * add test * fix * add tests, one failing * fix * flesh out await_waterfall message * tidy up * dry out * unused * tweak * tidy up * TODO * tweak * tidy up * remove TODO * unused export * add optimisation back * revert unneeded changes * revert * update some tests * more * more * move some code * rename * WIP * unset context synchronously * remove unused argument * Apply suggestions from code review Co-authored-by: Simon H <[email protected]> * add comment * add comment * use queue_micro_task in createSubscriber * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js Co-authored-by: Elliott Johnson <[email protected]> * prettier --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Simon Holthausen <[email protected]> Co-authored-by: Elliott Johnson <[email protected]>
1 parent 82f6481 commit 0672e48

File tree

204 files changed

+5777
-799
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

204 files changed

+5777
-799
lines changed

.changeset/eleven-weeks-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: support `await` in components when using the `experimental.async` compiler option

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ jobs:
4343
- run: pnpm test
4444
env:
4545
CI: true
46+
TestNoAsync:
47+
permissions: {}
48+
runs-on: ubuntu-latest
49+
timeout-minutes: 10
50+
steps:
51+
- uses: actions/checkout@v4
52+
- uses: pnpm/action-setup@v4
53+
- uses: actions/setup-node@v4
54+
with:
55+
node-version: 22
56+
cache: pnpm
57+
- run: pnpm install --frozen-lockfile
58+
- run: pnpm playwright install chromium
59+
- run: pnpm test runtime-runes
60+
env:
61+
CI: true
62+
SVELTE_NO_ASYNC: true
4663
Lint:
4764
permissions: {}
4865
runs-on: ubuntu-latest

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
22

3+
### async_derived_orphan
4+
5+
```
6+
Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
7+
```
8+
9+
In Svelte there are two types of reaction — [`$derived`](/docs/svelte/$derived) and [`$effect`](/docs/svelte/$effect). Deriveds can be created anywhere, because they run _lazily_ and can be [garbage collected](https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection) if nothing references them. Effects, by contrast, keep running eagerly whenever their dependencies change, until they are destroyed.
10+
11+
Because of this, effects can only be created inside other effects (or [effect roots](/docs/svelte/$effect#$effect.root), such as the one that is created when you first mount a component) so that Svelte knows when to destroy them.
12+
13+
Some sleight of hand occurs when a derived contains an `await` expression: Since waiting until we read `{await getPromise()}` to call `getPromise` would be too late, we use an effect to instead call it proactively, notifying Svelte when the value is available. But since we're using an effect, we can only create asynchronous deriveds inside another effect.
14+
315
### bind_invalid_checkbox_value
416

517
```
@@ -68,12 +80,28 @@ Effect cannot be created inside a `$derived` value that was not itself created i
6880
`%rune%` can only be used inside an effect (e.g. during component initialisation)
6981
```
7082

83+
### effect_pending_outside_reaction
84+
85+
```
86+
`$effect.pending()` can only be called inside an effect or derived
87+
```
88+
7189
### effect_update_depth_exceeded
7290

7391
```
7492
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
7593
```
7694

95+
### flush_sync_in_effect
96+
97+
```
98+
Cannot use `flushSync` inside an effect
99+
```
100+
101+
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
102+
103+
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
104+
77105
### get_abort_signal_outside_reaction
78106

79107
```
@@ -116,6 +144,14 @@ Rest element properties of `$props()` such as `%property%` are readonly
116144
The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
117145
```
118146

147+
### set_context_after_init
148+
149+
```
150+
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
151+
```
152+
153+
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
154+
119155
### state_descriptors_fixed
120156

121157
```

documentation/docs/98-reference/.generated/client-warnings.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,67 @@ function add() {
3434
}
3535
```
3636
37+
### await_reactivity_loss
38+
39+
```
40+
Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
41+
```
42+
43+
Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this...
44+
45+
```js
46+
let total = $derived(await a + b);
47+
```
48+
49+
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution.
50+
51+
This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this...
52+
53+
```js
54+
async function sum() {
55+
return await a + b;
56+
}
57+
58+
let total = $derived(await sum());
59+
```
60+
61+
...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function:
62+
63+
```js
64+
async function sum(a, b) {
65+
return await a + b;
66+
}
67+
68+
let total = $derived(await sum(a, b));
69+
```
70+
71+
### await_waterfall
72+
73+
```
74+
An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
75+
```
76+
77+
In a case like this...
78+
79+
```js
80+
let a = $derived(await one());
81+
let b = $derived(await two());
82+
```
83+
84+
...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary.
85+
86+
(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.)
87+
88+
You can solve this by creating the promises first and _then_ awaiting them:
89+
90+
```js
91+
let aPromise = $derived(one());
92+
let bPromise = $derived(two());
93+
94+
let a = $derived(await aPromise);
95+
let b = $derived(await bPromise);
96+
```
97+
3798
### binding_property_non_reactive
3899
39100
```

documentation/docs/98-reference/.generated/compile-errors.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ Expected token %token%
480480
Expected whitespace
481481
```
482482

483+
### experimental_async
484+
485+
```
486+
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
487+
```
488+
483489
### export_undefined
484490

485491
```
@@ -534,6 +540,12 @@ The arguments keyword cannot be used within the template or at the top level of
534540
%message%
535541
```
536542

543+
### legacy_await_invalid
544+
545+
```
546+
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
547+
```
548+
537549
### legacy_export_invalid
538550

539551
```

documentation/docs/98-reference/.generated/shared-errors.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
22

3+
### await_outside_boundary
4+
5+
```
6+
Cannot await outside a `<svelte:boundary>` with a `pending` snippet
7+
```
8+
9+
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:
10+
11+
```svelte
12+
<svelte:boundary>
13+
<p>{await getData()}</p>
14+
15+
{#snippet pending()}
16+
<p>loading...</p>
17+
{/snippet}
18+
</svelte:boundary>
19+
```
20+
21+
This restriction may be lifted in a future version of Svelte.
22+
323
### invalid_default_snippet
424

525
```

eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@ export default [
4949
},
5050
rules: {
5151
'@typescript-eslint/await-thenable': 'error',
52-
'@typescript-eslint/prefer-promise-reject-errors': 'error',
5352
'@typescript-eslint/require-await': 'error',
5453
'no-console': 'error',
5554
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
5655
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
5756
'object-shorthand': 'off',
57+
// eslint is being a dummy here too
58+
'@typescript-eslint/prefer-promise-reject-errors': 'off',
5859
'no-var': 'off',
5960

6061
// TODO: enable these rules and run `pnpm lint:fix`

packages/svelte/messages/client-errors/errors.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## async_derived_orphan
2+
3+
> Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
4+
5+
In Svelte there are two types of reaction — [`$derived`](/docs/svelte/$derived) and [`$effect`](/docs/svelte/$effect). Deriveds can be created anywhere, because they run _lazily_ and can be [garbage collected](https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection) if nothing references them. Effects, by contrast, keep running eagerly whenever their dependencies change, until they are destroyed.
6+
7+
Because of this, effects can only be created inside other effects (or [effect roots](/docs/svelte/$effect#$effect.root), such as the one that is created when you first mount a component) so that Svelte knows when to destroy them.
8+
9+
Some sleight of hand occurs when a derived contains an `await` expression: Since waiting until we read `{await getPromise()}` to call `getPromise` would be too late, we use an effect to instead call it proactively, notifying Svelte when the value is available. But since we're using an effect, we can only create asynchronous deriveds inside another effect.
10+
111
## bind_invalid_checkbox_value
212

313
> Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
@@ -44,10 +54,22 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
4454

4555
> `%rune%` can only be used inside an effect (e.g. during component initialisation)
4656
57+
## effect_pending_outside_reaction
58+
59+
> `$effect.pending()` can only be called inside an effect or derived
60+
4761
## effect_update_depth_exceeded
4862

4963
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
5064
65+
## flush_sync_in_effect
66+
67+
> Cannot use `flushSync` inside an effect
68+
69+
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
70+
71+
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
72+
5173
## get_abort_signal_outside_reaction
5274

5375
> `getAbortSignal()` can only be called inside an effect or derived
@@ -76,6 +98,12 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
7698

7799
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
78100
101+
## set_context_after_init
102+
103+
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
104+
105+
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
106+
79107
## state_descriptors_fixed
80108

81109
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,63 @@ function add() {
3030
}
3131
```
3232
33+
## await_reactivity_loss
34+
35+
> Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
36+
37+
Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this...
38+
39+
```js
40+
let total = $derived(await a + b);
41+
```
42+
43+
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution.
44+
45+
This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this...
46+
47+
```js
48+
async function sum() {
49+
return await a + b;
50+
}
51+
52+
let total = $derived(await sum());
53+
```
54+
55+
...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function:
56+
57+
```js
58+
async function sum(a, b) {
59+
return await a + b;
60+
}
61+
62+
let total = $derived(await sum(a, b));
63+
```
64+
65+
## await_waterfall
66+
67+
> An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
68+
69+
In a case like this...
70+
71+
```js
72+
let a = $derived(await one());
73+
let b = $derived(await two());
74+
```
75+
76+
...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary.
77+
78+
(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.)
79+
80+
You can solve this by creating the promises first and _then_ awaiting them:
81+
82+
```js
83+
let aPromise = $derived(one());
84+
let bPromise = $derived(two());
85+
86+
let a = $derived(await aPromise);
87+
let b = $derived(await bPromise);
88+
```
89+
3390
## binding_property_non_reactive
3491
3592
> `%binding%` is binding to a non-reactive property

packages/svelte/messages/compile-errors/script.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
7070

7171
> `$effect()` can only be used as an expression statement
7272
73+
## experimental_async
74+
75+
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
76+
7377
## export_undefined
7478

7579
> `%name%` is not defined
@@ -98,6 +102,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
98102

99103
> The arguments keyword cannot be used within the template or at the top level of a component
100104
105+
## legacy_await_invalid
106+
107+
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
108+
101109
## legacy_export_invalid
102110

103111
> Cannot use `export let` in runes mode — use `$props()` instead

0 commit comments

Comments
 (0)