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 25855163bf.

* 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 <sejohnson@torchcloudconsulting.com>

* Update packages/svelte/src/index-client.js

Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>

* Update packages/svelte/src/internal/client/runtime.js

Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>

* unused

* Update packages/svelte/src/internal/client/reactivity/sources.js

Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>

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

Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>

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

Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>

* 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 <sejohnson@torchcloudconsulting.com>

* 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 <5968653+dummdidumm@users.noreply.github.com>

* 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 <sejohnson@torchcloudconsulting.com>

* prettier

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
pull/16364/head
Rich Harris 2 months ago committed by GitHub
parent 82f648157e
commit 0672e48223
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support `await` in components when using the `experimental.async` compiler option

@ -43,6 +43,23 @@ jobs:
- run: pnpm test
env:
CI: true
TestNoAsync:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm playwright install chromium
- run: pnpm test runtime-runes
env:
CI: true
SVELTE_NO_ASYNC: true
Lint:
permissions: {}
runs-on: ubuntu-latest

@ -1,5 +1,17 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### async_derived_orphan
```
Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
```
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.
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.
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.
### bind_invalid_checkbox_value
```
@ -68,12 +80,28 @@ Effect cannot be created inside a `$derived` value that was not itself created i
`%rune%` can only be used inside an effect (e.g. during component initialisation)
```
### effect_pending_outside_reaction
```
`$effect.pending()` can only be called inside an effect or derived
```
### effect_update_depth_exceeded
```
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
```
### flush_sync_in_effect
```
Cannot use `flushSync` inside an effect
```
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.
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### get_abort_signal_outside_reaction
```
@ -116,6 +144,14 @@ Rest element properties of `$props()` such as `%property%` are readonly
The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
```
### set_context_after_init
```
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
```
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### state_descriptors_fixed
```

@ -34,6 +34,67 @@ function add() {
}
```
### await_reactivity_loss
```
Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
```
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...
```js
let total = $derived(await a + b);
```
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution.
This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this...
```js
async function sum() {
return await a + b;
}
let total = $derived(await sum());
```
...`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:
```js
async function sum(a, b) {
return await a + b;
}
let total = $derived(await sum(a, b));
```
### await_waterfall
```
An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
```
In a case like this...
```js
let a = $derived(await one());
let b = $derived(await two());
```
...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.
(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.)
You can solve this by creating the promises first and _then_ awaiting them:
```js
let aPromise = $derived(one());
let bPromise = $derived(two());
let a = $derived(await aPromise);
let b = $derived(await bPromise);
```
### binding_property_non_reactive
```

@ -480,6 +480,12 @@ Expected token %token%
Expected whitespace
```
### experimental_async
```
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
```
### export_undefined
```
@ -534,6 +540,12 @@ The arguments keyword cannot be used within the template or at the top level of
%message%
```
### legacy_await_invalid
```
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
```
### legacy_export_invalid
```

@ -1,5 +1,25 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### await_outside_boundary
```
Cannot await outside a `<svelte:boundary>` with 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) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
### invalid_default_snippet
```

@ -49,12 +49,13 @@ export default [
},
rules: {
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'@typescript-eslint/require-await': 'error',
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// 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
'object-shorthand': 'off',
// eslint is being a dummy here too
'@typescript-eslint/prefer-promise-reject-errors': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`

@ -1,3 +1,13 @@
## async_derived_orphan
> Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
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.
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.
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.
## bind_invalid_checkbox_value
> 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
> `%rune%` can only be used inside an effect (e.g. during component initialisation)
## effect_pending_outside_reaction
> `$effect.pending()` can only be called inside an effect or derived
## effect_update_depth_exceeded
> 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
## flush_sync_in_effect
> Cannot use `flushSync` inside an effect
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.
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## get_abort_signal_outside_reaction
> `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
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## set_context_after_init
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## state_descriptors_fixed
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.

@ -30,6 +30,63 @@ function add() {
}
```
## await_reactivity_loss
> Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
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...
```js
let total = $derived(await a + b);
```
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution.
This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this...
```js
async function sum() {
return await a + b;
}
let total = $derived(await sum());
```
...`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:
```js
async function sum(a, b) {
return await a + b;
}
let total = $derived(await sum(a, b));
```
## await_waterfall
> An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
In a case like this...
```js
let a = $derived(await one());
let b = $derived(await two());
```
...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.
(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.)
You can solve this by creating the promises first and _then_ awaiting them:
```js
let aPromise = $derived(one());
let bPromise = $derived(two());
let a = $derived(await aPromise);
let b = $derived(await bPromise);
```
## binding_property_non_reactive
> `%binding%` is binding to a non-reactive property

@ -70,6 +70,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> `$effect()` can only be used as an expression statement
## experimental_async
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
## export_undefined
> `%name%` is not defined
@ -98,6 +102,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> The arguments keyword cannot be used within the template or at the top level of a component
## legacy_await_invalid
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead

@ -1,3 +1,21 @@
## await_outside_boundary
> Cannot await outside a `<svelte:boundary>` with 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) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

@ -59,6 +59,9 @@
"./internal/disclose-version": {
"default": "./src/internal/disclose-version.js"
},
"./internal/flags/async": {
"default": "./src/internal/flags/async.js"
},
"./internal/flags/legacy": {
"default": "./src/internal/flags/legacy.js"
},

@ -170,6 +170,15 @@ export function effect_invalid_placement(node) {
e(node, 'effect_invalid_placement', `\`$effect()\` can only be used as an expression statement\nhttps://svelte.dev/e/effect_invalid_placement`);
}
/**
* Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function experimental_async(node) {
e(node, 'experimental_async', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async`);
}
/**
* `%name%` is not defined
* @param {null | number | NodeLike} node
@ -235,6 +244,15 @@ export function invalid_arguments_usage(node) {
e(node, 'invalid_arguments_usage', `The arguments keyword cannot be used within the template or at the top level of a component\nhttps://svelte.dev/e/invalid_arguments_usage`);
}
/**
* Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function legacy_await_invalid(node) {
e(node, 'legacy_await_invalid', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`);
}
/**
* Cannot use `export let` in runes mode use `$props()` instead
* @param {null | number | NodeLike} node

@ -145,7 +145,10 @@ export function migrate(source, { filename, use_ts } = {}) {
...validate_component_options({}, ''),
...parsed_options,
customElementOptions,
filename: filename ?? '(unknown)'
filename: filename ?? '(unknown)',
experimental: {
async: true
}
};
const str = new MagicString(source);

@ -295,6 +295,8 @@ export default function element(parser) {
} else {
element.tag = get_attribute_expression(definition);
}
element.metadata.expression = create_expression_metadata();
}
if (is_top_level_script_or_style) {

@ -1,4 +1,4 @@
/** @import { Comment, Expression, Node, Program } from 'estree' */
/** @import { Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
@ -22,6 +22,7 @@ import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
@ -140,6 +141,7 @@ const visitors = {
AttachTag,
Attribute,
AwaitBlock,
AwaitExpression,
BindDirective,
CallExpression,
ClassBody,
@ -211,9 +213,14 @@ function js(script, root, allow_reactive_declarations, parent) {
body: []
};
const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent);
const { scope, scopes, has_await } = create_scopes(
ast,
root,
allow_reactive_declarations,
parent
);
return { ast, scope, scopes };
return { ast, scope, scopes, has_await };
}
/**
@ -244,7 +251,7 @@ export function analyze_module(source, options) {
state.set_source(source);
const ast = parse(source, comments, false, false);
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -261,12 +268,13 @@ export function analyze_module(source, options) {
/** @type {Analysis} */
const analysis = {
module: { ast, scope, scopes },
module: { ast, scope, scopes, has_await },
name: options.filename,
accessors: false,
runes: true,
immutable: true,
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
};
@ -314,7 +322,12 @@ export function analyze_component(root, source, options) {
const module = js(root.module, scope_root, false, null);
const instance = js(root.instance, scope_root, true, module.scope);
const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope);
const { scope, scopes, has_await } = create_scopes(
root.fragment,
scope_root,
false,
instance.scope
);
/** @type {Template} */
const template = { ast: root.fragment, scope, scopes };
@ -422,7 +435,9 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename);
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
const runes =
options.runes ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
if (!runes) {
for (let check of synthetic_stores_legacy_check) {
@ -512,7 +527,8 @@ export function analyze_component(root, source, options) {
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set()
snippets: new Set(),
async_deriveds: new Set()
};
state.reset({

@ -0,0 +1,30 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
if (context.state.expression) {
context.state.expression.has_await = true;
suspend = true;
}
// disallow top-level `await` or `await` in template expressions
// unless a) in runes mode and b) opted into `experimental.async`
if (suspend) {
if (!context.state.options.experimental.async) {
e.experimental_async(node);
}
if (!context.state.analysis.runes) {
e.legacy_await_invalid(node);
}
}
context.next();
}

@ -7,6 +7,7 @@ import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders';
import { create_expression_metadata } from '../../nodes.js';
/**
* @param {CallExpression} node
@ -163,6 +164,13 @@ export function CallExpression(node, context) {
break;
case '$effect.pending':
if (context.state.expression) {
context.state.expression.has_state = true;
}
break;
case '$inspect':
if (node.arguments.length < 1) {
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
@ -227,7 +235,19 @@ export function CallExpression(node, context) {
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
if (rune === '$derived') {
const expression = create_expression_metadata();
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
expression
});
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
}
} else if (rune === '$inspect') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {
context.next();

@ -32,6 +32,7 @@ export function StyleDirective(node, context) {
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
node.metadata.expression.has_await ||= chunk.metadata.expression.has_await;
}
}
}

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
const valid = ['onerror', 'failed'];
const valid = ['onerror', 'failed', 'pending'];
/**
* @param {AST.SvelteBoundary} node

@ -62,5 +62,17 @@ export function SvelteElement(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, parent_element: null });
context.visit(node.tag, {
...context.state,
expression: node.metadata.expression
});
for (const attribute of node.attributes) {
context.visit(attribute);
}
context.visit(node.fragment, {
...context.state,
parent_element: null
});
}

@ -12,6 +12,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js';
@ -88,6 +89,7 @@ const visitors = {
AssignmentExpression,
Attribute,
AwaitBlock,
AwaitExpression,
BinaryExpression,
BindDirective,
BlockStatement,
@ -162,6 +164,7 @@ export function client_component(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
instance_level_snippets: [],
module_level_snippets: [],
@ -350,7 +353,7 @@ export function client_component(analysis, options) {
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
let component_block = b.block([
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
@ -358,10 +361,49 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
.../** @type {ESTree.Statement[]} */ (template.body)
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
]);
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
if (analysis.instance.has_await) {
const body = b.function_declaration(
b.id('$$body'),
should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],
b.block([
b.var('$$unsuspend', b.call('$.suspend')),
...component_block.body,
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body),
b.stmt(b.call('$$unsuspend'))
]),
true
);
state.hoisted.push(body);
component_block = b.block([
b.var('fragment', b.call('$.comment')),
b.var('node', b.call('$.first_child', b.id('fragment'))),
b.stmt(b.call(body.id, b.id('node'), should_inject_props && b.id('$$props'))),
b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment')))
]);
} else {
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
// trick esrap into including comments
component_block.loc = instance.loc;
@ -398,12 +440,6 @@ export function client_component(analysis, options) {
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
// we want the cleanup function for the stores to run as the very last thing
// so that it can effectively clean up the store subscription even after the user effects runs
if (should_inject_context) {
@ -469,14 +505,6 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props'))));
}
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
// Merge hoisted statements into module body.
// Ensure imports are on top, with the order preserved, then module body, then hoisted statements
/** @type {ESTree.ImportDeclaration[]} */
@ -537,6 +565,10 @@ export function client_component(analysis, options) {
);
}
if (options.experimental.async) {
body.unshift(b.imports([], 'svelte/internal/flags/async'));
}
if (!analysis.runes) {
body.unshift(b.imports([], 'svelte/internal/flags/legacy'));
}
@ -670,7 +702,9 @@ export function client_module(analysis, options) {
scopes: analysis.module.scopes,
state_fields: new Map(),
transform: {},
in_constructor: false
in_constructor: false,
in_derived: false,
is_instance: false
};
const module = /** @type {ESTree.Program} */ (

@ -21,6 +21,14 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
readonly in_derived: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly transform: Record<
string,
{
@ -41,7 +49,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
readonly is_instance: boolean;
readonly store_to_invalidate?: string;
/** Stuff that happens before the render effect(s) */

@ -0,0 +1,126 @@
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument));
const tla = context.state.is_instance && context.state.scope.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
return b.call(b.await(b.call('$.save', argument)));
}
// in dev, note which values are read inside a reactive expression,
// but don't track them
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
}
return argument === node.argument ? node : { ...node, argument };
}
/**
* @param {Context} context
*/
function is_reactive_expression(context) {
if (context.state.in_derived) {
return true;
}
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {Context} context
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(context, node) {
let i = context.path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -44,10 +44,11 @@ export function CallExpression(node, context) {
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (rune === '$derived') fn = b.thunk(fn);
let fn = /** @type {Expression} */ (
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
return b.call('$.derived', fn);
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
case '$state.snapshot':
@ -63,6 +64,9 @@ export function CallExpression(node, context) {
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
);
case '$effect.pending':
return b.call('$.pending');
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);

@ -312,11 +312,9 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
if (dev && node.metadata.keyed) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
const { has_await } = node.metadata.expression;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -326,7 +324,7 @@ export function EachBlock(node, context) {
const args = [
context.state.node,
b.literal(flags),
b.thunk(collection),
thunk,
key_function,
b.arrow(render_args, b.block(declarations.concat(block.body)))
];
@ -337,7 +335,26 @@ export function EachBlock(node, context) {
);
}
context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')];
if (dev && node.metadata.keyed) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([get_collection]),
b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
)
)
);
} else {
context.state.init.push(...statements);
}
}
/**

@ -47,9 +47,7 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' ||
trimmed[0].type === 'TitleElement' ||
(trimmed[0].type === 'IfBlock' && trimmed[0].elseif));
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function FunctionDeclaration(node, context) {
const state = { ...context.state, in_constructor: false };
const state = { ...context.state, in_constructor: false, in_derived: false };
if (node.metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);

@ -11,7 +11,9 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@ -20,7 +22,7 @@ export function HtmlTag(node, context) {
b.call(
'$.html',
context.state.node,
b.thunk(expression),
b.thunk(html),
is_svg && b.true,
is_mathml && b.true,
is_ignored(node, 'hydration_html_changed') && b.true
@ -28,5 +30,18 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
context.state.init.push(statement);
if (node.metadata.expression.has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
)
)
);
} else {
context.state.init.push(statement);
}
}

@ -13,37 +13,32 @@ export function IfBlock(node, context) {
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const consequent_id = context.state.scope.generate('consequent');
const consequent_id = b.id(context.state.scope.generate('consequent'));
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
let alternate_id;
if (node.alternate) {
alternate_id = context.state.scope.generate('alternate');
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
const nodes = node.alternate.nodes;
let alternate_args = [b.id('$$anchor')];
if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) {
alternate_args.push(b.id('$$elseif'));
}
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
}
const test = build_expression(context, node.test, node.metadata.expression);
const { has_await } = node.metadata.expression;
const expression = build_expression(context, node.test, node.metadata.expression);
const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
/** @type {Expression[]} */
const args = [
node.elseif ? b.id('$$anchor') : context.state.node,
context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.if(
test,
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
b.stmt(b.call('$$render', consequent_id)),
alternate_id && b.stmt(b.call('$$render', alternate_id, b.literal(false)))
)
])
)
@ -71,10 +66,23 @@ export function IfBlock(node, context) {
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
args.push(b.id('$$elseif'));
args.push(b.true);
}
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
context.state.init.push(b.block(statements));
if (has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
)
)
);
} else {
context.state.init.push(b.block(statements));
}
}

@ -11,14 +11,28 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const key = build_expression(context, node.expression, node.metadata.expression);
const { has_await } = node.metadata.expression;
const expression = build_expression(context, node.expression, node.metadata.expression);
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(
add_svelte_meta(
b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)),
node,
'key'
)
let statement = add_svelte_meta(
b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)),
node,
'key'
);
if (has_await) {
statement = b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
)
);
}
context.state.init.push(statement);
}

@ -253,7 +253,10 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value)
(value, metadata) =>
metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -319,7 +322,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.has_await)
) &&
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
@ -465,16 +472,18 @@ export function build_class_directives_object(
) {
let properties = [];
let has_call_or_state = false;
let has_await = false;
for (const d of class_directives) {
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = b.object(properties);
return has_call_or_state ? memoizer.add(directives) : directives;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
}
/**
@ -492,6 +501,7 @@ export function build_style_directives_object(
const important = b.object([]);
let has_call_or_state = false;
let has_await = false;
for (const d of style_directives) {
const expression =
@ -503,11 +513,12 @@ export function build_style_directives_object(
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state ? memoizer.add(directives) : directives;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
}
/**
@ -629,7 +640,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? state.memoizer.add(value) : value
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
);
const evaluated = context.state.scope.evaluate(value);

@ -1,9 +1,9 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { add_svelte_meta, build_expression } from './shared/utils.js';
import { add_svelte_meta, build_expression, Memoizer } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -12,32 +12,34 @@ import { add_svelte_meta, build_expression } from './shared/utils.js';
export function RenderTag(node, context) {
context.state.template.push_comment();
const expression = unwrap_optional(node.expression);
const callee = expression.callee;
const raw_args = expression.arguments;
const call = unwrap_optional(node.expression);
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
let thunk = b.thunk(
build_expression(context, /** @type {Expression} */ (raw_args[i]), node.metadata.arguments[i])
);
const { has_call } = node.metadata.arguments[i];
const memoizer = new Memoizer();
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
if (has_call) {
const id = b.id(context.state.scope.generate('render_arg'));
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', thunk)));
args.push(b.thunk(b.call('$.get', id)));
} else {
args.push(thunk);
let expression = build_expression(context, arg, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
}
args.push(b.thunk(expression));
}
memoizer.apply();
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
let snippet_function = build_expression(
context,
/** @type {Expression} */ (callee),
/** @type {Expression} */ (call.callee),
node.metadata.expression
);
@ -47,7 +49,7 @@ export function RenderTag(node, context) {
snippet_function = b.logical('??', snippet_function, b.id('$.noop'));
}
context.state.init.push(
statements.push(
add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
@ -55,7 +57,7 @@ export function RenderTag(node, context) {
)
);
} else {
context.state.init.push(
statements.push(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
@ -67,4 +69,21 @@ export function RenderTag(node, context) {
)
);
}
const async_values = memoizer.async_values();
if (async_values) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
memoizer.async_values(),
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

@ -33,7 +33,10 @@ export function SlotElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? b.call('$.get', memoizer.add(value)) : value)
(value, metadata) =>
metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value
);
if (attribute.name === 'name') {
@ -70,5 +73,20 @@ export function SlotElement(node, context) {
b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback))
);
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
const async_values = memoizer.async_values();
if (async_values) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
async_values,
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

@ -1,4 +1,4 @@
/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */
/** @import { BlockStatement, Statement, Expression, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -71,7 +71,7 @@ export function SvelteBoundary(node, context) {
hoisted.push(snippet);
if (child.expression.name === 'failed') {
if (['failed', 'pending'].includes(child.expression.name)) {
props.properties.push(b.prop('init', child.expression, child.expression));
}

@ -32,7 +32,7 @@ export function SvelteElement(node, context) {
const style_directives = [];
/** @type {ExpressionStatement[]} */
const lets = [];
const statements = [];
// Create a temporary context which picks up the init/update statements.
// They'll then be added to the function parameter of $.element
@ -64,7 +64,7 @@ export function SvelteElement(node, context) {
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
statements.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state));
inner_context.state.after_update.push(b.stmt(handler));
@ -73,9 +73,6 @@ export function SvelteElement(node, context) {
}
}
// Let bindings first, they can be used on attributes
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
if (
attributes.length === 1 &&
attributes[0].type === 'Attribute' &&
@ -96,14 +93,10 @@ export function SvelteElement(node, context) {
);
}
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));
const { has_await } = node.metadata.expression;
if (dev) {
if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@ -123,9 +116,16 @@ export function SvelteElement(node, context) {
).body
);
if (dev) {
if (node.fragment.nodes.length > 0) {
statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
const location = dev && locator(node.start);
context.state.init.push(
statements.push(
b.stmt(
b.call(
'$.element',
@ -138,4 +138,19 @@ export function SvelteElement(node, context) {
)
)
);
if (has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

@ -1,7 +1,7 @@
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
@ -20,7 +20,7 @@ export function VariableDeclaration(node, context) {
if (context.state.analysis.runes) {
for (const declarator of node.declarations) {
const init = declarator.init;
const init = /** @type {Expression} */ (declarator.init);
const rune = get_rune(init, context.state.scope);
if (
@ -194,16 +194,38 @@ export function VariableDeclaration(node, context) {
}
if (rune === '$derived' || rune === '$derived.by') {
const is_async = context.state.analysis.async_deriveds.has(
/** @type {CallExpression} */ (init)
);
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(
declarator.id,
dev ? b.call('$.tag', call, b.literal(declarator.id.name)) : call
)
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
} else {
if (rune === '$derived') expression = b.thunk(expression);
let call = b.call('$.derived', expression);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);

@ -129,11 +129,12 @@ export function build_component(node, component_name, context) {
(events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression))
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
)
);
@ -147,7 +148,9 @@ export function build_component(node, component_name, context) {
attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) => {
// TODO put the derived in the local block
return metadata.has_call ? b.call('$.get', memoizer.add(value)) : value;
return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}).value
)
);
@ -166,20 +169,24 @@ export function build_component(node, component_name, context) {
attribute.value,
context,
(value, metadata) => {
if (!metadata.has_state) return value;
if (!metadata.has_state && !metadata.has_await) return value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component (e.g. `active={i === index}`)
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
return should_wrap_in_derived ? b.call('$.get', memoizer.add(value)) : value;
const should_wrap_in_derived =
metadata.has_await ||
get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}
);
@ -489,5 +496,18 @@ export function build_component(node, component_name, context) {
memoizer.apply();
const async_values = memoizer.async_values();
if (async_values) {
return b.stmt(
b.call(
'$.async',
anchor,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
)
);
}
return statements.length > 1 ? b.block(statements) : statements[0];
}

@ -1,4 +1,4 @@
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
@ -33,7 +33,7 @@ export function build_attribute_effect(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? memoizer.add(value) : value
metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value
);
if (
@ -50,8 +50,8 @@ export function build_attribute_effect(
} else {
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
value = memoizer.add(value);
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = memoizer.add(value, attribute.metadata.expression.has_await);
}
values.push(b.spread(value));
@ -87,6 +87,7 @@ export function build_attribute_effect(
element_id,
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
memoizer.async_values(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -118,7 +119,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
return {
value: memoize(expression, chunk.metadata.expression),
has_state: chunk.metadata.expression.has_state
has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.has_await
};
}
@ -151,7 +152,9 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call ? context.state.memoizer.add(value) : value;
return metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value;
});
/** @type {Identifier | undefined} */
@ -165,7 +168,9 @@ export function build_set_class(element, node_id, attribute, class_directives, c
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
has_state ||= class_directives.some(
(d) => d.metadata.expression.has_state || d.metadata.expression.has_await
);
if (has_state) {
previous_id = b.id(context.state.scope.generate('classes'));
@ -219,7 +224,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? context.state.memoizer.add(value) : value
metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value
);
/** @type {Identifier | undefined} */
@ -233,7 +238,9 @@ export function build_set_style(node_id, attribute, style_directives, context) {
if (style_directives.length) {
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
has_state ||= style_directives.some(
(d) => d.metadata.expression.has_state || d.metadata.expression.has_await
);
if (has_state) {
previous_id = b.id(context.state.scope.generate('styles'));

@ -9,7 +9,7 @@ import { build_hoisted_params } from '../../utils.js';
export const visit_function = (node, context) => {
const metadata = node.metadata;
let state = { ...context.state, in_constructor: false };
let state = { ...context.state, in_constructor: false, in_derived: false };
if (node.type === 'FunctionExpression') {
const parent = /** @type {Node} */ (context.path.at(-1));

@ -18,19 +18,23 @@ export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#sync = [];
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#async = [];
/**
* @param {Expression} expression
* @param {boolean} has_await
*/
add(expression) {
add(expression, has_await) {
const id = b.id('#'); // filled in later
this.#sync.push({ id, expression });
(has_await ? this.#async : this.#sync).push({ id, expression });
return id;
}
apply() {
return this.#sync.map((memo, i) => {
return [...this.#async, ...this.#sync].map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});
@ -42,6 +46,15 @@ export class Memoizer {
);
}
async_ids() {
return this.#async.map((memo) => memo.id);
}
async_values() {
if (this.#async.length === 0) return;
return b.array(this.#async.map((memo) => b.thunk(memo.expression, true)));
}
sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
@ -59,7 +72,8 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value)
memoize = (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
) {
/** @type {Expression[]} */
const expressions = [];
@ -68,6 +82,7 @@ export function build_template_chunk(
const quasis = [quasi];
let has_state = false;
let has_await = false;
for (let i = 0; i < values.length; i++) {
const node = values[i];
@ -90,7 +105,8 @@ export function build_template_chunk(
const evaluated = state.scope.evaluate(value);
has_state ||= node.metadata.expression.has_state && !evaluated.is_known;
has_await ||= node.metadata.expression.has_await;
has_state ||= has_await || (node.metadata.expression.has_state && !evaluated.is_known);
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
@ -146,8 +162,9 @@ export function build_template_chunk(
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const ids = state.memoizer.apply();
const values = state.memoizer.sync_values();
const { memoizer } = state;
const ids = memoizer.apply();
return b.stmt(
b.call(
@ -158,8 +175,8 @@ export function build_render_statement(state) {
? state.update[0].expression
: b.block(state.update)
),
values,
values && !state.analysis.runes && b.id('$.derived_safe_equal')
memoizer.sync_values(),
memoizer.async_values()
)
);
}

@ -10,6 +10,7 @@ import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
@ -44,6 +45,7 @@ import { SvelteBoundary } from './visitors/SvelteBoundary.js';
const global_visitors = {
_: set_scope,
AssignmentExpression,
AwaitExpression,
CallExpression,
ClassBody,
ExpressionStatement,

@ -0,0 +1,25 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`,
// allow it, otherwise error
if (
context.state.scope.function_depth === 0 ||
context.path.some(
(node) =>
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression'
)
) {
return context.next();
}
return b.call('$.await_outside_boundary');
}

@ -25,6 +25,10 @@ export function CallExpression(node, context) {
return b.arrow([], b.block([]));
}
if (rune === '$effect.pending') {
return b.literal(0);
}
if (rune === '$state' || rune === '$state.raw') {
return node.arguments[0] ? context.visit(node.arguments[0]) : b.void0;
}

@ -10,29 +10,18 @@ import { block_close, block_open } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
const test = /** @type {Expression} */ (context.visit(node.test));
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent);
context.state.template.push(if_statement, block_close);
const alternate = node.alternate
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
let index = 1;
let alt = node.alternate;
while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent));
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(`<!--[${index++}-->`)))
);
if_statement = if_statement.alternate = b.if(
/** @type {Expression} */ (context.visit(elseif.test)),
alternate
);
alt = elseif.alternate;
}
consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]);
if_statement.alternate.body.unshift(
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
context.state.template.push(b.if(test, consequent, alternate), block_close);
}

@ -3,15 +3,36 @@
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/utils.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
context.state.template.push(
b.literal(BLOCK_OPEN),
/** @type {BlockStatement} */ (context.visit(node.fragment)),
b.literal(BLOCK_CLOSE)
context.state.template.push(b.literal(BLOCK_OPEN));
// if this has a `pending` snippet, render it
const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
);
const pending_snippet = /** @type {AST.SnippetBlock} */ (
node.fragment.nodes.find(
(node) => node.type === 'SnippetBlock' && node.expression.name === 'pending'
)
);
if (pending_attribute) {
const value = build_attribute_value(pending_attribute.value, context, false, true);
context.state.template.push(b.call(value, b.id('$$payload')));
} else if (pending_snippet) {
context.state.template.push(
/** @type {BlockStatement} */ (context.visit(pending_snippet.body))
);
} else {
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
}
context.state.template.push(b.literal(BLOCK_CLOSE));
}

@ -66,7 +66,8 @@ export function create_expression_metadata() {
has_state: false,
has_call: false,
has_member_expression: false,
has_assignment: false
has_assignment: false,
has_await: false
};
}

@ -933,7 +933,25 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
};
let has_await = false;
walk(ast, state, {
AwaitExpression(node, context) {
// this doesn't _really_ belong here, but it allows us to
// automatically opt into runes mode on encountering
// blocking awaits, without doing an additional walk
// before the analysis occurs
// TODO remove this in Svelte 7.0 or whenever we get rid of legacy support
has_await ||= context.path.every(
({ type }) =>
type !== 'ArrowFunctionExpression' &&
type !== 'FunctionExpression' &&
type !== 'FunctionDeclaration'
);
context.next();
},
// references
Identifier(node, { path, state }) {
const parent = path.at(-1);
@ -1290,6 +1308,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
return {
has_await,
scope,
scopes
};

@ -1,21 +1,19 @@
import type { AST, Binding, StateField } from '#compiler';
import type {
AssignmentExpression,
CallExpression,
ClassBody,
Comment,
Identifier,
LabeledStatement,
Node,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { StateCreationRuneName } from '../../utils.js';
import type { AnalysisState } from './2-analyze/types.js';
export interface Js {
ast: Program;
scope: Scope;
scopes: Map<AST.SvelteNode, Scope>;
has_await: boolean;
}
export interface Template {
@ -46,6 +44,9 @@ export interface Analysis {
// TODO figure out if we can move this to ComponentAnalysis
accessors: boolean;
/** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>;
}
export interface ComponentAnalysis extends Analysis {

@ -224,6 +224,11 @@ export interface ModuleCompileOptions {
* Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it.
*/
warningFilter?: (warning: Warning) => boolean;
/** Experimental options */
experimental?: {
/** Allow `await` keyword in deriveds, template expressions, and the top level of components */
async?: boolean;
};
}
// The following two somewhat scary looking types ensure that certain types are required but can be undefined still
@ -287,6 +292,8 @@ export interface ExpressionMetadata {
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression contains `await` */
has_await: boolean;
/** True if the expression includes a member expression */
has_member_expression: boolean;
/** True if the expression includes an assignment or an update */

@ -366,6 +366,7 @@ export namespace AST {
tag: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
/**
* `true` if this is an svg element. The boolean may not be accurate because
* the tag is dynamic, but we do our best to infer it from the template.

@ -1,4 +1,5 @@
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
@ -30,16 +31,17 @@ export function assignment_pattern(left, right) {
/**
* @param {Array<ESTree.Pattern>} params
* @param {ESTree.BlockStatement | ESTree.Expression} body
* @param {boolean} async
* @returns {ESTree.ArrowFunctionExpression}
*/
export function arrow(params, body) {
export function arrow(params, body, async = false) {
return {
type: 'ArrowFunctionExpression',
params,
body,
expression: body.type !== 'BlockStatement',
generator: false,
async: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}
@ -100,7 +102,7 @@ export function labeled(name, body) {
/**
* @param {string | ESTree.Expression} callee
* @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined)} args
* @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined | null)} args
* @returns {ESTree.CallExpression}
*/
export function call(callee, ...args) {
@ -216,16 +218,17 @@ export function export_default(declaration) {
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
* @param {ESTree.BlockStatement} body
* @param {boolean} async
* @returns {ESTree.FunctionDeclaration}
*/
export function function_declaration(id, params, body) {
export function function_declaration(id, params, body, async = false) {
return {
type: 'FunctionDeclaration',
id,
params,
body,
generator: false,
async: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}
@ -421,19 +424,32 @@ export function template(elements, expressions) {
* @returns {ESTree.Expression}
*/
export function thunk(expression, async = false) {
const fn = arrow([], expression);
if (async) fn.async = true;
return unthunk(fn);
return unthunk(arrow([], expression, async));
}
/**
* Replace "(arg) => func(arg)" to "func"
* @param {ESTree.Expression} expression
* @param {ESTree.ArrowFunctionExpression} expression
* @returns {ESTree.Expression}
*/
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
let has_await = false;
walk(expression.body.argument, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
if (!has_await) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
if (
expression.type === 'ArrowFunctionExpression' &&
expression.async === false &&
expression.body.type === 'CallExpression' &&
expression.body.callee.type === 'Identifier' &&

@ -41,7 +41,11 @@ const common = {
return input;
}),
warningFilter: fun(() => true)
warningFilter: fun(() => true),
experimental: object({
async: boolean(false)
})
};
export const validate_module_options =

@ -42,6 +42,8 @@ export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
// we use a list of ignorable runtime warnings because not every runtime warning
// can be ignored and we want to keep the validation for svelte-ignore in place
export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
'await_waterfall',
'await_reactivity_loss',
'state_snapshot_uncloneable',
'binding_property_non_reactive',
'hydration_attribute_changed',

@ -237,8 +237,8 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
export { flushSync } from './internal/client/runtime.js';
export { flushSync } from './internal/client/reactivity/batch.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack } from './internal/client/runtime.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -35,6 +35,8 @@ export function unmount() {
export async function tick() {}
export async function settled() {}
export { getAbortSignal } from './internal/server/abort-signal.js';
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';

@ -18,8 +18,13 @@ export const EFFECT_TRANSPARENT = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const EFFECT_IS_UPDATING = 1 << 20;
export const USER_EFFECT = 1 << 21;
export const USER_EFFECT = 1 << 20;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;
export const ERROR_VALUE = 1 << 23;
export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');

@ -1,10 +1,11 @@
/** @import { ComponentContext, DevStackEntry } from '#client' */
/** @import { ComponentContext, DevStackEntry, Effect } from '#client' */
import { DEV } from 'esm-env';
import * as e from './errors.js';
import { source } from './reactivity/sources.js';
import { active_effect, active_reaction } from './runtime.js';
import { create_user_effect } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;
@ -96,6 +97,16 @@ export function getContext(key) {
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f;
var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
if (!valid) {
e.set_context_after_init();
}
}
context_map.set(key, context);
return context;
}
@ -138,18 +149,9 @@ export function push(props, runes = false, fn) {
e: null,
s: props,
x: null,
l: null
l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null
};
if (legacy_mode_flag && !runes) {
component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) {
// component function
component_context.function = fn;

@ -7,6 +7,7 @@ import {
CLEAN,
DERIVED,
EFFECT,
ASYNC,
MAYBE_DIRTY,
RENDER_EFFECT,
ROOT_EFFECT
@ -39,6 +40,8 @@ export function log_effect_tree(effect, depth = 0) {
label = 'boundary';
} else if ((flags & BLOCK_EFFECT) !== 0) {
label = 'block';
} else if ((flags & ASYNC) !== 0) {
label = 'async';
} else if ((flags & BRANCH_EFFECT) !== 0) {
label = 'branch';
} else if ((flags & RENDER_EFFECT) !== 0) {

@ -2,7 +2,7 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
@ -26,7 +26,7 @@ function log_entry(signal, entry) {
return;
}
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const type = (signal.f & (DERIVED | ASYNC)) !== 0 ? '$derived' : '$state';
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
const style = dirty

@ -0,0 +1,26 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import { get_pending_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
var boundary = get_pending_boundary();
boundary.update_pending_count(1);
flatten([], expressions, (values) => {
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
fn(node, ...values);
} finally {
boundary.update_pending_count(-1);
}
});
}

@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { set_active_effect, set_active_reaction } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
@ -22,6 +22,7 @@ import {
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
import { flushSync } from '../../reactivity/batch.js';
const PENDING = 0;
const THEN = 1;

@ -1,4 +1,4 @@
/** @import { Effect, TemplateNode, } from '#client' */
/** @import { Effect, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
@ -6,6 +6,7 @@ import { block, branch, destroy_effect, pause_effect } from '../../reactivity/ef
import {
active_effect,
active_reaction,
get,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
@ -17,12 +18,20 @@ import {
remove_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { Batch } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
/**
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* pending?: (anchor: Node) => void;
* }} BoundaryProps
*/
@ -39,6 +48,11 @@ export function boundary(node, props, children) {
}
export class Boundary {
pending = false;
/** @type {Boundary | null} */
parent;
/** @type {TemplateNode} */
#anchor;
@ -57,11 +71,39 @@ export class Boundary {
/** @type {Effect | null} */
#main_effect = null;
/** @type {Effect | null} */
#pending_effect = null;
/** @type {Effect | null} */
#failed_effect = null;
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
#pending_count = 0;
#is_creating_fallback = false;
/**
* A source containing the number of pending async deriveds/expressions.
* Only created if `$effect.pending()` is used inside the boundary,
* otherwise updating the source results in needless `Batch.ensure()`
* calls followed by no-op flushes
* @type {Source<number> | null}
*/
#effect_pending = null;
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#pending_count);
if (DEV) {
tag(this.#effect_pending, '$effect.pending()');
}
return () => {
this.#effect_pending = null;
};
});
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
@ -74,6 +116,10 @@ export class Boundary {
this.#hydrate_open = hydrate_node;
this.parent = /** @type {Effect} */ (active_effect).b;
this.pending = !!this.#props.pending;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
@ -81,10 +127,43 @@ export class Boundary {
hydrate_next();
}
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
const pending = this.#props.pending;
if (hydrating && pending) {
this.#pending_effect = branch(() => pending(this.#anchor));
// future work: when we have some form of async SSR, we will
// need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given
// boundary, and hydrate accordingly
queueMicrotask(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.pending = false;
}
});
} else {
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
}
}
}, flags);
@ -93,6 +172,10 @@ export class Boundary {
}
}
has_pending_snippet() {
return !!this.#props.pending;
}
/**
* @param {() => Effect | null} fn
*/
@ -114,22 +197,85 @@ export class Boundary {
}
}
#show_pending_snippet() {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
move_effect(this.#main_effect, this.#offscreen_fragment);
}
if (this.#pending_effect === null) {
this.#pending_effect = branch(() => pending(this.#anchor));
}
}
/** @param {1 | -1} d */
#update_pending_count(d) {
this.#pending_count += d;
if (this.#pending_count === 0) {
this.pending = false;
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
this.#pending_effect = null;
});
}
if (this.#offscreen_fragment) {
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
}
}
/** @param {1 | -1} d */
update_pending_count(d) {
if (this.has_pending_snippet()) {
this.#update_pending_count(d);
} else if (this.parent) {
this.parent.#update_pending_count(d);
}
queueMicrotask(() => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#pending_count);
}
});
}
get_effect_pending() {
this.#effect_pending_subscriber();
return get(/** @type {Source<number>} */ (this.#effect_pending));
}
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
const reset = () => {
this.#pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
this.pending = true;
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
}
};
// If we have nothing to capture the error, or if we hit an error while
@ -152,6 +298,11 @@ export class Boundary {
this.#main_effect = null;
}
if (this.#pending_effect) {
destroy_effect(this.#pending_effect);
this.#pending_effect = null;
}
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
@ -187,3 +338,49 @@ export class Boundary {
}
}
}
/**
*
* @param {Effect} effect
* @param {DocumentFragment} fragment
*/
function move_effect(effect, fragment) {
var node = effect.nodes_start;
var end = effect.nodes_end;
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
fragment.append(node);
node = next;
}
}
export function get_pending_boundary() {
var boundary = /** @type {Effect} */ (active_effect).b;
while (boundary !== null && !boundary.has_pending_snippet()) {
boundary = boundary.parent;
}
if (boundary === null) {
e.await_outside_boundary();
}
return boundary;
}
export function pending() {
if (active_effect === null) {
e.effect_pending_outside_reaction();
}
var boundary = active_effect.b;
if (boundary === null) {
return 0; // TODO eventually we will need this to be global
}
return boundary.get_effect_pending();
}

@ -1,4 +1,5 @@
/** @import { EachItem, EachState, Effect, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
@ -21,7 +22,8 @@ import {
clear_text_content,
create_text,
get_first_child,
get_next_sibling
get_next_sibling,
should_defer_append
} from '../operations.js';
import {
block,
@ -39,6 +41,7 @@ import { queue_micro_task } from '../task.js';
import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.js';
/**
* The row of a keyed each block that is currently updating. We track this
@ -66,9 +69,10 @@ export function index(_, i) {
* @param {EachState} state
* @param {EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {Map<any, EachItem>} items_map
*/
function pause_effects(state, items, controlled_anchor, items_map) {
function pause_effects(state, items, controlled_anchor) {
var items_map = state.items;
/** @type {TransitionManager[]} */
var transitions = [];
var length = items.length;
@ -137,6 +141,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var was_empty = false;
/** @type {Map<any, EachItem>} */
var offscreen_items = new Map();
// TODO: ideally we could use derived for runes mode but because of the ability
// to use a store which can be mutated, we can't do that here as mutating a store
// will still result in the collection array being the same from the store
@ -146,8 +153,45 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
return is_array(collection) ? collection : collection == null ? [] : array_from(collection);
});
/** @type {V[]} */
var array;
/** @type {Effect} */
var each_effect;
function commit() {
reconcile(
each_effect,
array,
state,
offscreen_items,
anchor,
render_fn,
flags,
get_key,
get_collection
);
if (fallback_fn !== null) {
if (array.length === 0) {
if (fallback) {
resume_effect(fallback);
} else {
fallback = branch(() => fallback_fn(anchor));
}
} else if (fallback !== null) {
pause_effect(fallback, () => {
fallback = null;
});
}
}
}
block(() => {
var array = get(each_array);
// store a reference to the effect so that we can update the start/end nodes in reconciliation
each_effect ??= /** @type {Effect} */ (active_effect);
array = get(each_array);
var length = array.length;
if (was_empty && length === 0) {
@ -219,21 +263,56 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
}
if (!hydrating) {
reconcile(array, state, anchor, render_fn, flags, get_key, get_collection);
}
if (hydrating) {
if (length === 0 && fallback_fn) {
fallback = branch(() => fallback_fn(anchor));
}
} else {
if (should_defer_append()) {
var keys = new Set();
var batch = /** @type {Batch} */ (current_batch);
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
var existing = state.items.get(key) ?? offscreen_items.get(key);
if (existing) {
// update before reconciliation, to trigger any async updates
if ((flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0) {
update_item(existing, value, i, flags);
}
} else {
item = create_item(
null,
state,
null,
null,
value,
key,
i,
render_fn,
flags,
get_collection,
true
);
offscreen_items.set(key, item);
}
if (fallback_fn !== null) {
if (length === 0) {
if (fallback) {
resume_effect(fallback);
} else {
fallback = branch(() => fallback_fn(anchor));
keys.add(key);
}
} else if (fallback !== null) {
pause_effect(fallback, () => {
fallback = null;
});
for (const [key, item] of state.items) {
if (!keys.has(key)) {
batch.skipped_effects.add(item.e);
}
}
batch.add_callback(commit);
} else {
commit();
}
}
@ -259,8 +338,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
/**
* Add, remove, or reorder items output by an each block as its input changes
* @template V
* @param {Effect} each_effect
* @param {Array<V>} array
* @param {EachState} state
* @param {Map<any, EachItem>} offscreen_items
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: MaybeSource<V>, index: number | Source<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
@ -268,7 +349,17 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
* @param {() => V[]} get_collection
* @returns {void}
*/
function reconcile(array, state, anchor, render_fn, flags, get_key, get_collection) {
function reconcile(
each_effect,
array,
state,
offscreen_items,
anchor,
render_fn,
flags,
get_key,
get_collection
) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
@ -320,23 +411,39 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (item === undefined) {
var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor;
prev = create_item(
child_anchor,
state,
prev,
prev === null ? state.first : prev.next,
value,
key,
i,
render_fn,
flags,
get_collection
);
var pending = offscreen_items.get(key);
if (pending !== undefined) {
offscreen_items.delete(key);
items.set(key, pending);
var next = prev ? prev.next : current;
link(state, prev, pending);
link(state, pending, next);
move(pending, next, anchor);
prev = pending;
} else {
var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor;
prev = create_item(
child_anchor,
state,
prev,
prev === null ? state.first : prev.next,
value,
key,
i,
render_fn,
flags,
get_collection
);
}
items.set(key, prev);
@ -455,7 +562,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti
}
}
pause_effects(state, to_destroy, controlled_anchor, items);
pause_effects(state, to_destroy, controlled_anchor);
}
}
@ -468,8 +575,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti
});
}
/** @type {Effect} */ (active_effect).first = state.first && state.first.e;
/** @type {Effect} */ (active_effect).last = prev && prev.e;
each_effect.first = state.first && state.first.e;
each_effect.last = prev && prev.e;
for (var unused of offscreen_items.values()) {
destroy_effect(unused.e);
}
offscreen_items.clear();
}
/**
@ -493,7 +606,7 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {Node | null} anchor
* @param {EachState} state
* @param {EachItem | null} prev
* @param {EachItem | null} next
@ -503,6 +616,7 @@ function update_item(item, value, index, type) {
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @param {boolean} [deferred]
* @returns {EachItem}
*/
function create_item(
@ -515,7 +629,8 @@ function create_item(
index,
render_fn,
flags,
get_collection
get_collection,
deferred
) {
var previous_each_item = current_each_item;
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
@ -549,13 +664,20 @@ function create_item(
current_each_item = item;
try {
item.e = branch(() => render_fn(anchor, v, i, get_collection), hydrating);
if (anchor === null) {
var fragment = document.createDocumentFragment();
fragment.append((anchor = create_text()));
}
item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection), hydrating);
item.e.prev = prev && prev.e;
item.e.next = next && next.e;
if (prev === null) {
state.first = item;
if (!deferred) {
state.first = item;
}
} else {
prev.next = item;
prev.e.next = item.e;
@ -583,7 +705,7 @@ function move(item, next, anchor) {
var dest = next ? /** @type {TemplateNode} */ (next.e.nodes_start) : anchor;
var node = /** @type {TemplateNode} */ (item.e.nodes_start);
while (node !== end) {
while (node !== null && node !== end) {
var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
dest.before(node);
node = next_node;

@ -1,4 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
@ -10,16 +11,20 @@ import {
set_hydrating
} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { create_text, should_defer_append } from '../operations.js';
import { current_batch } from '../../reactivity/batch.js';
// TODO reinstate https://github.com/sveltejs/svelte/pull/15250
/**
* @param {TemplateNode} node
* @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn
* @param {[number,number]} [elseif]
* @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating && root_index === 0) {
export function if_block(node, fn, elseif = false) {
if (hydrating) {
hydrate_next();
}
@ -34,45 +39,56 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
/** @type {UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0;
var flags = elseif ? EFFECT_TRANSPARENT : 0;
var has_branch = false;
const set_branch = (
/** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn,
flag = true
) => {
const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
has_branch = true;
update_branch(flag, fn);
};
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
function commit() {
if (offscreen_fragment !== null) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
var active = condition ? consequent_effect : alternate_effect;
var inactive = condition ? alternate_effect : consequent_effect;
if (active) {
resume_effect(active);
}
if (inactive) {
pause_effect(inactive, () => {
if (condition) {
alternate_effect = null;
} else {
consequent_effect = null;
}
});
}
}
const update_branch = (
/** @type {boolean | null} */ new_condition,
/** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn
/** @type {null | ((anchor: Node) => void)} */ fn
) => {
if (condition === (condition = new_condition)) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
if (hydrating && hydrate_index !== -1) {
if (root_index === 0) {
const data = read_hydration_instruction(anchor);
if (data === HYDRATION_START) {
hydrate_index = 0;
} else if (data === HYDRATION_START_ELSE) {
hydrate_index = Infinity;
} else {
hydrate_index = parseInt(data.substring(1));
if (hydrate_index !== hydrate_index) {
// if hydrate_index is NaN
// we set an invalid index to force mismatch
hydrate_index = condition ? Infinity : -1;
}
}
}
const is_else = hydrate_index > root_index;
if (hydrating) {
const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
@ -82,34 +98,35 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
hydrate_index = -1; // ignore hydration in next else if
}
}
if (condition) {
if (consequent_effect) {
resume_effect(consequent_effect);
} else if (fn) {
consequent_effect = branch(() => fn(anchor));
}
var defer = should_defer_append();
var target = anchor;
if (alternate_effect) {
pause_effect(alternate_effect, () => {
alternate_effect = null;
});
}
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
if (condition) {
consequent_effect ??= fn && branch(() => fn(target));
} else {
if (alternate_effect) {
resume_effect(alternate_effect);
} else if (fn) {
alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index]));
}
alternate_effect ??= fn && branch(() => fn(target));
}
if (consequent_effect) {
pause_effect(consequent_effect, () => {
consequent_effect = null;
});
}
if (defer) {
var batch = /** @type {Batch} */ (current_batch);
var active = condition ? consequent_effect : alternate_effect;
var inactive = condition ? alternate_effect : consequent_effect;
if (active) batch.skipped_effects.delete(active);
if (inactive) batch.skipped_effects.add(inactive);
batch.add_callback(commit);
} else {
commit();
}
if (mismatch) {

@ -1,9 +1,12 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
import { is_runes } from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
import { current_batch } from '../../reactivity/batch.js';
/**
* @template V
@ -12,7 +15,7 @@ import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
* @param {(anchor: Node) => TemplateNode | void} render_fn
* @returns {void}
*/
export function key_block(node, get_key, render_fn) {
export function key(node, get_key, render_fn) {
if (hydrating) {
hydrate_next();
}
@ -25,15 +28,48 @@ export function key_block(node, get_key, render_fn) {
/** @type {Effect} */
var effect;
/** @type {Effect} */
var pending_effect;
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
var changed = is_runes() ? not_equal : safe_not_equal;
function commit() {
if (effect) {
pause_effect(effect);
}
if (offscreen_fragment !== null) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
effect = pending_effect;
}
block(() => {
if (changed(key, (key = get_key()))) {
if (effect) {
pause_effect(effect);
var target = anchor;
var defer = should_defer_append();
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
effect = branch(() => render_fn(anchor));
pending_effect = branch(() => render_fn(target));
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
}
});

@ -1,7 +1,10 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { EFFECT_TRANSPARENT } from '#client/constants';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { current_batch } from '../../reactivity/batch.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
/**
* @template P
@ -24,16 +27,50 @@ export function component(node, get_component, render_fn) {
/** @type {Effect | null} */
var effect;
block(() => {
if (component === (component = get_component())) return;
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
/** @type {Effect | null} */
var pending_effect = null;
function commit() {
if (effect) {
pause_effect(effect);
effect = null;
}
if (offscreen_fragment) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
effect = pending_effect;
pending_effect = null;
}
block(() => {
if (component === (component = get_component())) return;
var defer = should_defer_append();
if (component) {
effect = branch(() => render_fn(anchor, component));
var target = anchor;
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
pending_effect = branch(() => render_fn(target, component));
}
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
}, EFFECT_TRANSPARENT);

@ -21,8 +21,8 @@ import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { derived } from '../../reactivity/deriveds.js';
import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
@ -462,66 +462,67 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
/**
* @param {Element & ElementCSSInlineStyle} element
* @param {(...expressions: any) => Record<string | symbol, any>} fn
* @param {Array<() => any>} thunks
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {string} [css_hash]
* @param {boolean} [skip_warning]
*/
export function attribute_effect(
element,
fn,
thunks = [],
sync = [],
async = [],
css_hash,
skip_warning = false,
d = derived
skip_warning = false
) {
const deriveds = thunks.map(d);
flatten(sync, async, (values) => {
/** @type {Record<string | symbol, any> | undefined} */
var prev = undefined;
/** @type {Record<string | symbol, any> | undefined} */
var prev = undefined;
/** @type {Record<symbol, Effect>} */
var effects = {};
/** @type {Record<symbol, Effect>} */
var effects = {};
var is_select = element.nodeName === 'SELECT';
var inited = false;
var is_select = element.nodeName === 'SELECT';
var inited = false;
block(() => {
var next = fn(...values.map(get));
/** @type {Record<string | symbol, any>} */
var current = set_attributes(element, prev, next, css_hash, skip_warning);
block(() => {
var next = fn(...deriveds.map(get));
/** @type {Record<string | symbol, any>} */
var current = set_attributes(element, prev, next, css_hash, skip_warning);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value);
}
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value);
}
for (let symbol of Object.getOwnPropertySymbols(effects)) {
if (!next[symbol]) destroy_effect(effects[symbol]);
}
for (let symbol of Object.getOwnPropertySymbols(effects)) {
if (!next[symbol]) destroy_effect(effects[symbol]);
}
for (let symbol of Object.getOwnPropertySymbols(next)) {
var n = next[symbol];
for (let symbol of Object.getOwnPropertySymbols(next)) {
var n = next[symbol];
if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) {
if (effects[symbol]) destroy_effect(effects[symbol]);
effects[symbol] = branch(() => attach(element, () => n));
}
if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) {
if (effects[symbol]) destroy_effect(effects[symbol]);
effects[symbol] = branch(() => attach(element, () => n));
current[symbol] = n;
}
current[symbol] = n;
}
prev = current;
});
prev = current;
});
if (is_select) {
var select = /** @type {HTMLSelectElement} */ (element);
if (is_select) {
var select = /** @type {HTMLSelectElement} */ (element);
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
init_select(select);
});
}
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
init_select(select);
});
}
inited = true;
inited = true;
});
}
/**

@ -64,6 +64,15 @@ export function bind_value(input, get, set = get) {
var value = get();
if (input === document.activeElement) {
// Never rewrite the contents of a focused input. We can get here if, for example,
// an update is deferred because of async work depending on the input:
//
// <input bind:value={query}>
// <p>{await find(query)}</p>
return;
}
if (is_numberlike_input(input) && value === to_number(input.value)) {
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
return;

@ -1,9 +1,11 @@
/** @import { TemplateNode } from '#client' */
/** @import { Effect, TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { TEXT_NODE } from '#client/constants';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -204,6 +206,19 @@ export function clear_text_content(node) {
node.textContent = '';
}
/**
* Returns `true` if we're updating the current block, for example `condition` in
* an `{#if condition}` block just changed. In this case, the branch should be
* appended (or removed) at the same time as other updates within the
* current `<svelte:boundary>`
*/
export function should_defer_append() {
if (!async_mode_flag) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;
}
/**
*
* @param {string} tag

@ -1,11 +1,11 @@
/** @import { Effect } from '#client' */
/** @import { Derived, Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
import { ERROR_VALUE, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
import { define_property, get_descriptor } from '../shared/utils.js';
import { active_effect } from './runtime.js';
import { active_effect, active_reaction } from './runtime.js';
const adjustments = new WeakMap();
@ -13,7 +13,13 @@ const adjustments = new WeakMap();
* @param {unknown} error
*/
export function handle_error(error) {
var effect = /** @type {Effect} */ (active_effect);
var effect = active_effect;
// for unowned deriveds, don't throw until we read the value
if (effect === null) {
/** @type {Derived} */ (active_reaction).f |= ERROR_VALUE;
return error;
}
if (DEV && error instanceof Error && !adjustments.has(error)) {
adjustments.set(error, get_adjustments(error, effect));
@ -23,11 +29,14 @@ export function handle_error(error) {
// if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (!effect.parent && error instanceof Error) {
apply_adjustments(error);
}
throw error;
}
// @ts-expect-error
effect.fn(error);
/** @type {Boundary} */ (effect.b).error(error);
} else {
// otherwise we bubble up the effect tree ourselves
invoke_error_boundary(error, effect);

@ -4,6 +4,22 @@ import { DEV } from 'esm-env';
export * from '../shared/errors.js';
/**
* Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
* @returns {never}
*/
export function async_derived_orphan() {
if (DEV) {
const error = new Error(`async_derived_orphan\nCannot create a \`$derived(...)\` with an \`await\` expression outside of an effect tree\nhttps://svelte.dev/e/async_derived_orphan`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/async_derived_orphan`);
}
}
/**
* Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
* @returns {never}
@ -181,6 +197,22 @@ export function effect_orphan(rune) {
}
}
/**
* `$effect.pending()` can only be called inside an effect or derived
* @returns {never}
*/
export function effect_pending_outside_reaction() {
if (DEV) {
const error = new Error(`effect_pending_outside_reaction\n\`$effect.pending()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/effect_pending_outside_reaction`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_pending_outside_reaction`);
}
}
/**
* 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
* @returns {never}
@ -197,6 +229,22 @@ export function effect_update_depth_exceeded() {
}
}
/**
* Cannot use `flushSync` inside an effect
* @returns {never}
*/
export function flush_sync_in_effect() {
if (DEV) {
const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
}
}
/**
* `getAbortSignal()` can only be called inside an effect or derived
* @returns {never}
@ -313,6 +361,22 @@ export function rune_outside_svelte(rune) {
}
}
/**
* `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
* @returns {never}
*/
export function set_context_after_init() {
if (DEV) {
const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/set_context_after_init`);
}
}
/**
* Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
* @returns {never}

@ -9,10 +9,11 @@ export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace, tag, tag_proxy } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
export { async } from './dom/blocks/async.js';
export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
export { key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js';
@ -97,8 +98,15 @@ export {
props_id,
with_script
} from './dom/template.js';
export { user_derived as derived, derived_safe_equal } from './reactivity/deriveds.js';
export { save, track_reactivity_loss } from './reactivity/async.js';
export { flushSync as flush, suspend } from './reactivity/batch.js';
export {
async_derived,
user_derived as derived,
derived_safe_equal
} from './reactivity/deriveds.js';
export {
aborted,
effect_tracking,
effect_root,
legacy_pre_effect,
@ -129,13 +137,12 @@ export {
update_store,
mark_store_binding
} from './reactivity/store.js';
export { boundary } from './dom/blocks/boundary.js';
export { boundary, pending } from './dom/blocks/boundary.js';
export { set_text } from './render.js';
export {
get,
safe_get,
invalidate_inner_signals,
flushSync as flush,
tick,
untrack,
exclude_from_object,

@ -0,0 +1,127 @@
/** @import { Effect, Value } from '#client' */
import { DESTROYED } from '#client/constants';
import { DEV } from 'esm-env';
import { component_context, is_runes, set_component_context } from '../context.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from '../runtime.js';
import { current_batch } from './batch.js';
import {
async_derived,
current_async_effect,
derived,
derived_safe_equal,
set_from_async_derived
} from './deriveds.js';
/**
*
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
*/
export function flatten(sync, async, fn) {
const d = is_runes() ? derived : derived_safe_equal;
if (async.length === 0) {
fn(sync.map(d));
return;
}
var batch = current_batch;
var parent = /** @type {Effect} */ (active_effect);
var restore = capture();
var boundary = get_pending_boundary();
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
batch?.activate();
restore();
try {
fn([...sync.map(d), ...result]);
} catch (error) {
// ignore errors in blocks that have already been destroyed
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
}
batch?.deactivate();
unset_context();
})
.catch((error) => {
boundary.error(error);
});
}
/**
* Captures the current effect context so that we can restore it after
* some asynchronous work has happened (so that e.g. `await a + b`
* causes `b` to be registered as a dependency).
*/
function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
if (DEV) {
set_from_async_derived(null);
}
};
}
/**
* Wraps an `await` expression in such a way that the effect context that was
* active before the expression evaluated can be reapplied afterwards
* `await a + b` becomes `(await $.save(a))() + b`
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function save(promise) {
var restore = capture();
var value = await promise;
return () => {
restore();
return value;
};
}
/**
* Reset `current_async_effect` after the `promise` resolves, so
* that we can emit `await_reactivity_loss` warnings
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function track_reactivity_loss(promise) {
var previous_async_effect = current_async_effect;
var value = await promise;
return () => {
set_from_async_derived(previous_async_effect);
return value;
};
}
export function unset_context() {
set_active_effect(null);
set_active_reaction(null);
set_component_context(null);
if (DEV) set_from_async_derived(null);
}

@ -0,0 +1,603 @@
/** @import { Derived, Effect, Source } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
CLEAN,
DESTROYED,
DIRTY,
EFFECT,
ASYNC,
INERT,
RENDER_EFFECT,
ROOT_EFFECT,
USER_EFFECT
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import {
active_effect,
is_dirty,
is_updating_effect,
set_is_updating_effect,
set_signal_status,
update_effect,
write_version
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { old_values } from './sources.js';
import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';
/** @type {Set<Batch>} */
const batches = new Set();
/** @type {Batch | null} */
export let current_batch = null;
/**
* When time travelling, we re-evaluate deriveds based on the temporary
* values of their dependencies rather than their actual values, and cache
* the results in this map rather than on the deriveds themselves
* @type {Map<Derived, any> | null}
*/
export let batch_deriveds = null;
/** @type {Effect[]} Stack of effects, dev only */
export let dev_effect_stack = [];
/** @type {Effect[]} */
let queued_root_effects = [];
/** @type {Effect | null} */
let last_scheduled_effect = null;
let is_flushing = false;
export class Batch {
/**
* The current values of any sources that are updated in this batch
* They keys of this map are identical to `this.#previous`
* @type {Map<Source, any>}
*/
#current = new Map();
/**
* The values of any sources that are updated in this batch _before_ those updates took place.
* They keys of this map are identical to `this.#current`
* @type {Map<Source, any>}
*/
#previous = new Map();
/**
* When the batch is committed (and the DOM is updated), we need to remove old branches
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
* @type {Set<() => void>}
*/
#callbacks = new Set();
/**
* The number of async effects that are currently in flight
*/
#pending = 0;
/**
* A deferred that resolves when the batch is committed, used with `settled()`
* TODO replace with Promise.withResolvers once supported widely enough
* @type {{ promise: Promise<void>, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null}
*/
#deferred = null;
/**
* True if an async effect inside this batch resolved and
* its parent branch was already deleted
*/
#neutered = false;
/**
* Async effects (created inside `async_derived`) encountered during processing.
* These run after the rest of the batch has updated, since they should
* always have the latest values
* @type {Effect[]}
*/
#async_effects = [];
/**
* The same as `#async_effects`, but for effects inside a newly-created
* `<svelte:boundary>` these do not prevent the batch from committing
* @type {Effect[]}
*/
#boundary_async_effects = [];
/**
* Template effects and `$effect.pre` effects, which run when
* a batch is committed
* @type {Effect[]}
*/
#render_effects = [];
/**
* The same as `#render_effects`, but for `$effect` (which runs after)
* @type {Effect[]}
*/
#effects = [];
/**
* Block effects, which may need to re-run on subsequent flushes
* in order to update internal sources (e.g. each block items)
* @type {Effect[]}
*/
#block_effects = [];
/**
* A set of branches that still exist, but will be destroyed when this batch
* is committed we skip over these during `process`
* @type {Set<Effect>}
*/
skipped_effects = new Set();
/**
*
* @param {Effect[]} root_effects
*/
#process(root_effects) {
queued_root_effects = [];
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = null;
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
if (batches.size > 1) {
current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of this.#current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
for (const batch of batches) {
if (batch === this) continue;
for (const [source, previous] of batch.#previous) {
if (!current_values.has(source)) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = previous;
}
}
}
}
for (const root of root_effects) {
this.#traverse_effect_tree(root);
}
// if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit
if (this.#async_effects.length === 0 && this.#pending === 0) {
var render_effects = this.#render_effects;
var effects = this.#effects;
this.#render_effects = [];
this.#effects = [];
this.#block_effects = [];
this.#commit();
flush_queued_effects(render_effects);
flush_queued_effects(effects);
this.#deferred?.resolve();
} else {
// otherwise mark effects clean so they get scheduled on the next run
for (const e of this.#render_effects) set_signal_status(e, CLEAN);
for (const e of this.#effects) set_signal_status(e, CLEAN);
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
}
if (current_values) {
for (const [source, { v, wv }] of current_values) {
// reset the source to the current value (unless
// it got a newer value as a result of effects running)
if (source.wv <= wv) {
source.v = v;
}
}
batch_deriveds = null;
}
for (const effect of this.#async_effects) {
update_effect(effect);
}
for (const effect of this.#boundary_async_effects) {
update_effect(effect);
}
this.#async_effects = [];
this.#boundary_async_effects = [];
}
/**
* Traverse the effect tree, executing effects or stashing
* them for later execution as appropriate
* @param {Effect} root
*/
#traverse_effect_tree(root) {
root.f ^= CLEAN;
var effect = root.first;
while (effect !== null) {
var flags = effect.f;
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if (is_dirty(effect)) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
} else {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
update_effect(effect);
}
}
var child = effect.first;
if (child !== null) {
effect = child;
continue;
}
}
var parent = effect.parent;
effect = effect.next;
while (effect === null && parent !== null) {
effect = parent.next;
parent = parent.parent;
}
}
}
/**
* Associate a change to a given source with the current
* batch, noting its previous and current values
* @param {Source} source
* @param {any} value
*/
capture(source, value) {
if (!this.#previous.has(source)) {
this.#previous.set(source, value);
}
this.#current.set(source, source.v);
}
activate() {
current_batch = this;
}
deactivate() {
current_batch = null;
}
neuter() {
this.#neutered = true;
}
flush() {
if (queued_root_effects.length > 0) {
this.flush_effects();
} else {
this.#commit();
}
if (current_batch !== this) {
// this can happen if a `flushSync` occurred during `this.flush_effects()`,
// which is permitted in legacy mode despite being a terrible idea
return;
}
if (this.#pending === 0) {
batches.delete(this);
}
current_batch = null;
}
flush_effects() {
var was_updating_effect = is_updating_effect;
is_flushing = true;
try {
var flush_count = 0;
set_is_updating_effect(true);
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
this.#process(queued_root_effects);
old_values.clear();
}
} finally {
is_flushing = false;
set_is_updating_effect(was_updating_effect);
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
/**
* Append and remove branches to/from the DOM
*/
#commit() {
if (!this.#neutered) {
for (const fn of this.#callbacks) {
fn();
}
}
this.#callbacks.clear();
}
increment() {
this.#pending += 1;
}
decrement() {
this.#pending -= 1;
if (this.#pending === 0) {
for (const e of this.#render_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#block_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
this.#render_effects = [];
this.#effects = [];
this.flush();
}
}
/** @param {() => void} fn */
add_callback(fn) {
this.#callbacks.add(fn);
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batches.add(current_batch);
queueMicrotask(() => {
if (current_batch !== batch) {
// a flushSync happened in the meantime
return;
}
batch.flush();
});
}
return current_batch;
}
}
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* @template [T=void]
* @param {(() => T) | undefined} [fn]
* @returns {T}
*/
export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) {
e.flush_sync_in_effect();
}
var result;
const batch = Batch.ensure();
if (fn) {
batch.flush_effects();
result = fn();
}
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
if (batch === current_batch) {
batch.flush();
}
// this would be reset in `batch.flush_effects()` but since we are early returning here,
// we need to reset it here as well in case the first time there's 0 queued root effects
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
return /** @type {T} */ (result);
}
batch.flush_effects();
}
}
function log_effect_stack() {
// eslint-disable-next-line no-console
console.error(
'Last ten effects were: ',
dev_effect_stack.slice(-10).map((d) => d.fn)
);
dev_effect_stack = [];
}
function infinite_loop_guard() {
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
try {
invoke_error_boundary(error, last_scheduled_effect);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
invoke_error_boundary(error, last_scheduled_effect);
}
} else {
if (DEV) {
log_effect_stack();
}
throw error;
}
}
}
/**
* @param {Array<Effect>} effects
* @returns {void}
*/
function flush_queued_effects(effects) {
var length = effects.length;
if (length === 0) return;
for (var i = 0; i < length; i++) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0) {
if (is_dirty(effect)) {
var wv = write_version;
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
if (effect.teardown === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
}
}
// if state is written in a user effect, abort and re-schedule, lest we run
// effects that should be removed as a result of the state change
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
break;
}
}
}
}
for (; i < length; i += 1) {
schedule_effect(effects[i]);
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
var effect = (last_scheduled_effect = signal);
while (effect.parent !== null) {
effect = effect.parent;
var flags = effect.f;
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, bail out or we'll cause a second flush
if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) {
return;
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
effect.f ^= CLEAN;
}
}
queued_root_effects.push(effect);
}
export function suspend() {
var boundary = get_pending_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.pending;
boundary.update_pending_count(1);
if (!pending) batch.increment();
return function unsuspend() {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
unset_context();
};
}
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
batches.clear();
}

@ -1,6 +1,17 @@
/** @import { Derived, Effect } from '#client' */
/** @import { Derived, Effect, Source } from '#client' */
/** @import { Batch } from './batch.js'; */
import { DEV } from 'esm-env';
import { CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import {
ERROR_VALUE,
CLEAN,
DERIVED,
DIRTY,
EFFECT_PRESERVED,
MAYBE_DIRTY,
STALE_REACTION,
UNOWNED,
ASYNC
} from '#client/constants';
import {
active_reaction,
active_effect,
@ -14,12 +25,26 @@ import {
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import { destroy_effect } from './effects.js';
import { inspect_effects, set_inspect_effects } from './sources.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect } from './effects.js';
import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_deriveds, current_batch } from './batch.js';
import { unset_context } from './async.js';
/** @type {Effect | null} */
export let current_async_effect = null;
/** @param {Effect | null} v */
export function set_from_async_derived(v) {
current_async_effect = v;
}
export const recent_async_deriveds = new Set();
/**
* @template V
@ -65,6 +90,135 @@ export function derived(fn) {
return signal;
}
/**
* @template V
* @param {() => V | Promise<V>} fn
* @param {string} [location] If provided, print a warning if the value is not read immediately after update
* @returns {Promise<Source<V>>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function async_derived(fn, location) {
let parent = /** @type {Effect | null} */ (active_effect);
if (parent === null) {
e.async_derived_orphan();
}
var boundary = /** @type {Boundary} */ (parent.b);
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
/** @type {Promise<V> | null} */
var prev = null;
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
async_effect(() => {
if (DEV) current_async_effect = active_effect;
try {
var p = fn();
} catch (error) {
p = Promise.reject(error);
}
if (DEV) current_async_effect = null;
var r = () => p;
promise = prev?.then(r, r) ?? Promise.resolve(p);
prev = promise;
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.pending;
if (should_suspend) {
boundary.update_pending_count(1);
if (!pending) batch.increment();
}
/**
* @param {any} value
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
prev = null;
current_async_effect = null;
if (!pending) batch.activate();
if (error) {
if (error !== STALE_REACTION) {
signal.f |= ERROR_VALUE;
// @ts-expect-error the error is the wrong type, but we don't care
internal_set(signal, error);
}
} else {
if ((signal.f & ERROR_VALUE) !== 0) {
signal.f ^= ERROR_VALUE;
}
internal_set(signal, value);
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
setTimeout(() => {
if (recent_async_deriveds.has(signal)) {
w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal);
}
});
}
}
if (should_suspend) {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
}
unset_context();
};
promise.then(handler, (e) => handler(null, e || 'unknown'));
if (batch) {
return () => {
queueMicrotask(() => batch.neuter());
};
}
});
if (DEV) {
// add a flag that lets this be printed as a derived
// when using `$inspect.trace()`
signal.f |= ASYNC;
}
return new Promise((fulfil) => {
/** @param {Promise<V>} p */
function next(p) {
function go() {
if (p === promise) {
fulfil(signal);
} else {
// if the effect re-runs before the initial promise
// resolves, delay resolution until we have a value
next(promise);
}
}
p.then(go, go);
}
next(promise);
});
}
/**
* @template V
* @param {() => V} fn
@ -185,8 +339,12 @@ export function update_derived(derived) {
// cleanup function, or it will cache a stale value
if (is_destroying_effect) return;
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
if (batch_deriveds !== null) {
batch_deriveds.set(derived, derived.v);
} else {
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
set_signal_status(derived, status);
}
}

@ -7,7 +7,6 @@ import {
get,
is_destroying_effect,
remove_reactions,
schedule_effect,
set_active_reaction,
set_is_destroying_effect,
set_signal_status,
@ -32,17 +31,17 @@ import {
HEAD_EFFECT,
MAYBE_DIRTY,
EFFECT_PRESERVED,
BOUNDARY_EFFECT,
STALE_REACTION,
USER_EFFECT
USER_EFFECT,
ASYNC
} from '#client/constants';
import { set } from './sources.js';
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 } from './deriveds.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -93,6 +92,10 @@ function create_effect(type, fn, sync, push = true) {
}
}
if (parent !== null && (parent.f & INERT) !== 0) {
type |= INERT;
}
/** @type {Effect} */
var effect = {
ctx: component_context,
@ -137,7 +140,7 @@ function create_effect(type, fn, sync, push = true) {
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
(effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0;
(effect.f & EFFECT_PRESERVED) === 0;
if (!inert && push) {
if (parent !== null) {
@ -185,8 +188,13 @@ export function user_effect(fn) {
});
}
if (!active_reaction && active_effect && (active_effect.f & BRANCH_EFFECT) !== 0) {
// Top-level `$effect(...)` in a component — defer until mount
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
var flags = /** @type {Effect} */ (active_effect).f;
var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
if (defer) {
// Top-level `$effect(...)` in an unmounted component — defer until mount
var context = /** @type {ComponentContext} */ (component_context);
(context.e ??= []).push(fn);
} else {
@ -228,6 +236,7 @@ export function inspect_effect(fn) {
* @returns {() => void}
*/
export function effect_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true);
return () => {
@ -241,6 +250,7 @@ export function effect_root(fn) {
* @returns {(options?: { outro?: boolean }) => Promise<void>}
*/
export function component_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true);
return (options = {}) => {
@ -274,9 +284,10 @@ export function effect(fn) {
export function legacy_pre_effect(deps, fn) {
var context = /** @type {ComponentContextLegacy} */ (component_context);
/** @type {{ effect: null | Effect, ran: boolean }} */
var token = { effect: null, ran: false };
context.l.r1.push(token);
/** @type {{ effect: null | Effect, ran: boolean, deps: () => any }} */
var token = { effect: null, ran: false, deps };
context.l.$.push(token);
token.effect = render_effect(() => {
deps();
@ -286,7 +297,6 @@ export function legacy_pre_effect(deps, fn) {
if (token.ran) return;
token.ran = true;
set(context.l.r2, true);
untrack(fn);
});
}
@ -295,10 +305,10 @@ export function legacy_pre_effect_reset() {
var context = /** @type {ComponentContextLegacy} */ (component_context);
render_effect(() => {
if (!get(context.l.r2)) return;
// Run dirty `$:` statements
for (var token of context.l.r1) {
for (var token of context.l.$) {
token.deps();
var effect = token.effect;
// If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through
@ -313,8 +323,6 @@ export function legacy_pre_effect_reset() {
token.ran = false;
}
context.l.r2.v = false; // set directly to avoid rerunning this effect
});
}
@ -322,34 +330,27 @@ export function legacy_pre_effect_reset() {
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function render_effect(fn) {
return create_effect(RENDER_EFFECT, fn, true);
export function async_effect(fn) {
return create_effect(ASYNC | EFFECT_PRESERVED, fn, true);
}
/**
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} thunks
* @param {<T>(fn: () => T) => Derived<T>} d
* @param {() => void | (() => void)} fn
* @returns {Effect}
*/
export function template_effect(fn, thunks = [], d = derived) {
if (DEV) {
// wrap the effect so that we can decorate stack trace with `in {expression}`
// (TODO maybe there's a better approach?)
return render_effect(() => {
var outer = /** @type {Effect} */ (active_effect);
var inner = () => fn(...deriveds.map(get));
define_property(outer.fn, 'name', { value: '{expression}' });
define_property(inner, 'name', { value: '{expression}' });
const deriveds = thunks.map(d);
block(inner);
});
}
export function render_effect(fn, flags = 0) {
return create_effect(RENDER_EFFECT | flags, fn, true);
}
const deriveds = thunks.map(d);
return block(() => fn(...deriveds.map(get)));
/**
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
*/
export function template_effect(fn, sync = [], async = []) {
flatten(sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
});
}
/**
@ -357,7 +358,7 @@ export function template_effect(fn, thunks = [], d = derived) {
* @param {number} flags
*/
export function block(fn, flags = 0) {
var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
var effect = create_effect(BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
@ -369,7 +370,7 @@ export function block(fn, flags = 0) {
* @param {boolean} [push]
*/
export function branch(fn, push = true) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push);
return create_effect(BRANCH_EFFECT, fn, true, push);
}
/**
@ -606,6 +607,15 @@ function resume_children(effect, local) {
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;
// If a dependency of this effect changed while it was paused,
// schedule the effect to update. we don't use `is_dirty`
// here because we don't want to eagerly recompute a derived like
// `{#if foo}{foo.bar()}{/if}` if `foo` is now `undefined
if ((effect.f & CLEAN) === 0) {
set_signal_status(effect, DIRTY);
schedule_effect(effect);
}
var child = effect.first;
while (child !== null) {
@ -626,3 +636,8 @@ function resume_children(effect, local) {
}
}
}
export function aborted() {
var effect = /** @type {Effect} */ (active_effect);
return (effect.f & DESTROYED) !== 0;
}

@ -5,7 +5,6 @@ import {
active_effect,
untracked_writes,
get,
schedule_effect,
set_untracked_writes,
set_signal_status,
untrack,
@ -27,12 +26,14 @@ import {
UNOWNED,
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT
ROOT_EFFECT,
ASYNC
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
@ -139,7 +140,7 @@ export function set(source, value, should_proxy = false) {
// to ensure we error if state is set inside an inspect effect
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | INSPECT_EFFECT)) !== 0 &&
!current_sources?.includes(source)
) {
e.state_unsafe_mutation();
@ -172,6 +173,9 @@ export function internal_set(source, value) {
source.v = value;
const batch = Batch.ensure();
batch.capture(source, old_value);
if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt');
@ -218,6 +222,7 @@ export function internal_set(source, value) {
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (is_dirty(effect)) {
update_effect(effect);
}

@ -1,36 +1,36 @@
/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js';
import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js';
import {
destroy_block_effect_children,
destroy_effect_children,
execute_effect_teardown,
unlink_effect
execute_effect_teardown
} from './reactivity/effects.js';
import {
EFFECT,
DIRTY,
MAYBE_DIRTY,
CLEAN,
DERIVED,
UNOWNED,
DESTROYED,
INERT,
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
DISCONNECTED,
EFFECT_IS_UPDATING,
REACTION_IS_UPDATING,
STALE_REACTION,
USER_EFFECT
ERROR_VALUE
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js';
import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
import { tracing_mode_flag } from '../flags/index.js';
import {
destroy_derived_effects,
execute_derived,
current_async_effect,
recent_async_deriveds,
update_derived
} from './reactivity/deriveds.js';
import { async_mode_flag, tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
component_context,
@ -41,15 +41,23 @@ import {
set_dev_current_component_function,
set_dev_stack
} from './context.js';
import { handle_error, invoke_error_boundary } from './error-handling.js';
import * as w from './warnings.js';
import {
Batch,
batch_deriveds,
dev_effect_stack,
flushSync,
schedule_effect
} from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
let is_flushing = false;
export let is_updating_effect = false;
/** @type {Effect | null} */
let last_scheduled_effect = null;
let is_updating_effect = false;
/** @param {boolean} value */
export function set_is_updating_effect(value) {
is_updating_effect = value;
}
export let is_destroying_effect = false;
@ -58,15 +66,6 @@ export function set_is_destroying_effect(value) {
is_destroying_effect = value;
}
// Handle effect queues
/** @type {Effect[]} */
let queued_root_effects = [];
/** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions
/** @type {null | Reaction} */
export let active_reaction = null;
@ -94,7 +93,7 @@ export let current_sources = null;
/** @param {Value} value */
export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (active_reaction !== null && (!async_mode_flag || (active_reaction.f & DERIVED) !== 0)) {
if (current_sources === null) {
current_sources = [value];
} else {
@ -129,7 +128,7 @@ export function set_untracked_writes(value) {
* @type {number} Used by sources and deriveds for handling updates.
* Version starts from 1 so that unowned deriveds differentiate between a created effect and a run one for tracing
**/
let write_version = 1;
export let write_version = 1;
/** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */
let read_version = 0;
@ -182,8 +181,12 @@ export function is_dirty(reaction) {
var length = dependencies.length;
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
// then we need to re-connect the reaction to the dependency
if (is_disconnected || is_unowned_connected) {
// then we need to re-connect the reaction to the dependency, unless the effect has already been destroyed
// (which can happen if the derived is read by an async derived)
if (
(is_disconnected || is_unowned_connected) &&
(active_effect === null || (active_effect.f & DESTROYED) === 0)
) {
var derived = /** @type {Derived} */ (reaction);
var parent = derived.parent;
@ -241,7 +244,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
var reactions = signal.reactions;
if (reactions === null) return;
if (current_sources?.includes(signal)) {
if (!async_mode_flag && current_sources?.includes(signal)) {
return;
}
@ -287,14 +290,13 @@ export function update_reaction(reaction) {
untracking = false;
update_version = ++read_version;
reaction.f |= EFFECT_IS_UPDATING;
if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION);
reaction.ac = null;
}
try {
reaction.f |= REACTION_IS_UPDATING;
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;
@ -361,10 +363,15 @@ export function update_reaction(reaction) {
}
}
if ((reaction.f & ERROR_VALUE) !== 0) {
reaction.f ^= ERROR_VALUE;
}
return result;
} catch (error) {
handle_error(error);
return handle_error(error);
} finally {
reaction.f ^= REACTION_IS_UPDATING;
new_deps = previous_deps;
skipped_deps = previous_skipped_deps;
untracked_writes = previous_untracked_writes;
@ -374,8 +381,6 @@ export function update_reaction(reaction) {
set_component_context(previous_component_context);
untracking = previous_untracking;
update_version = previous_update_version;
reaction.f ^= EFFECT_IS_UPDATING;
}
}
@ -400,6 +405,7 @@ function remove_reaction(signal, dependency) {
}
}
}
// If the derived has no reactions, then we can disconnect it from the graph,
// allowing it to either reconnect in the future, or be GC'd by the VM.
if (
@ -500,253 +506,29 @@ export function update_effect(effect) {
}
}
function log_effect_stack() {
// eslint-disable-next-line no-console
console.error(
'Last ten effects were: ',
dev_effect_stack.slice(-10).map((d) => d.fn)
);
dev_effect_stack = [];
}
function infinite_loop_guard() {
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
try {
invoke_error_boundary(error, last_scheduled_effect);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
invoke_error_boundary(error, last_scheduled_effect);
}
} else {
if (DEV) {
log_effect_stack();
}
throw error;
}
}
}
function flush_queued_root_effects() {
var was_updating_effect = is_updating_effect;
try {
var flush_count = 0;
is_updating_effect = true;
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
var root_effects = queued_root_effects;
var length = root_effects.length;
queued_root_effects = [];
for (var i = 0; i < length; i++) {
var collected_effects = process_effects(root_effects[i]);
flush_queued_effects(collected_effects);
}
old_values.clear();
}
} finally {
is_flushing = false;
is_updating_effect = was_updating_effect;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
/**
* @param {Array<Effect>} effects
* @returns {void}
*/
function flush_queued_effects(effects) {
var length = effects.length;
if (length === 0) return;
for (var i = 0; i < length; i++) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0) {
if (is_dirty(effect)) {
var wv = write_version;
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
if (effect.teardown === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
}
}
// if state is written in a user effect, abort and re-schedule, lest we run
// effects that should be removed as a result of the state change
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
break;
}
}
}
}
for (; i < length; i += 1) {
schedule_effect(effects[i]);
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
if (!is_flushing) {
is_flushing = true;
queueMicrotask(flush_queued_root_effects);
}
var effect = (last_scheduled_effect = signal);
while (effect.parent !== null) {
effect = effect.parent;
var flags = effect.f;
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
effect.f ^= CLEAN;
}
}
queued_root_effects.push(effect);
}
/**
*
* This function both runs render effects and collects user effects in topological order
* from the starting effect passed in. Effects will be collected when they match the filtered
* bitwise flag passed in only. The collected effects array will be populated with all the user
* effects to be flushed.
*
* @param {Effect} root
* @returns {Effect[]}
*/
function process_effects(root) {
/** @type {Effect[]} */
var effects = [];
/** @type {Effect | null} */
var effect = root;
while (effect !== null) {
var flags = effect.f;
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
if (!is_skippable_branch && (flags & INERT) === 0) {
if ((flags & EFFECT) !== 0) {
effects.push(effect);
} else if (is_branch) {
effect.f ^= CLEAN;
} else {
if (is_dirty(effect)) {
update_effect(effect);
}
}
/** @type {Effect | null} */
var child = effect.first;
if (child !== null) {
effect = child;
continue;
}
}
var parent = effect.parent;
effect = effect.next;
while (effect === null && parent !== null) {
effect = parent.next;
parent = parent.parent;
}
}
return effects;
}
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* @template [T=void]
* @param {(() => T) | undefined} [fn]
* @returns {T}
* Returns a promise that resolves once any pending state changes have been applied.
* @returns {Promise<void>}
*/
export function flushSync(fn) {
var result;
if (fn) {
is_flushing = true;
flush_queued_root_effects();
is_flushing = true;
result = fn();
export async function tick() {
if (async_mode_flag) {
return new Promise((f) => requestAnimationFrame(() => f()));
}
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
// this would be reset in `flush_queued_root_effects` but since we are early returning here,
// we need to reset it here as well in case the first time there's 0 queued root effects
is_flushing = false;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
return /** @type {T} */ (result);
}
await Promise.resolve();
is_flushing = true;
flush_queued_root_effects();
}
// By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
flushSync();
}
/**
* Returns a promise that resolves once any pending state changes have been applied.
* Returns a promise that resolves once any state changes, and asynchronous work resulting from them,
* have resolved and the DOM has been updated
* @returns {Promise<void>}
*/
export async function tick() {
await Promise.resolve();
// By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
flushSync();
export function settled() {
return Batch.ensure().settled();
}
/**
@ -764,22 +546,44 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
if (!current_sources?.includes(signal)) {
// if we're in a derived that is being read inside an _async_ derived,
// it's possible that the effect was already destroyed. In this case,
// we don't add the dependency, because that would create a memory leak
var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0;
if (!destroyed && !current_sources?.includes(signal)) {
var deps = active_reaction.deps;
if (signal.rv < read_version) {
signal.rv = read_version;
// If the signal is accessing the same dependencies in the same
// order as it did last time, increment `skipped_deps`
// rather than updating `new_deps`, which creates GC cost
if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else if (!skip_reaction || !new_deps.includes(signal)) {
// Normally we can push duplicated dependencies to `new_deps`, but if we're inside
// an unowned derived because skip_reaction is true, then we need to ensure that
// we don't have duplicates
new_deps.push(signal);
if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) {
// we're in the effect init/update cycle
if (signal.rv < read_version) {
signal.rv = read_version;
// If the signal is accessing the same dependencies in the same
// order as it did last time, increment `skipped_deps`
// rather than updating `new_deps`, which creates GC cost
if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else if (!skip_reaction || !new_deps.includes(signal)) {
// Normally we can push duplicated dependencies to `new_deps`, but if we're inside
// an unowned derived because skip_reaction is true, then we need to ensure that
// we don't have duplicates
new_deps.push(signal);
}
}
} else {
// we're adding a dependency outside the init/update cycle
// (i.e. after an `await`)
(active_reaction.deps ??= []).push(signal);
var reactions = signal.reactions;
if (reactions === null) {
signal.reactions = [active_reaction];
} else if (!reactions.includes(active_reaction)) {
reactions.push(active_reaction);
}
}
}
@ -799,42 +603,50 @@ export function get(signal) {
}
}
if (is_derived && !is_destroying_effect) {
derived = /** @type {Derived} */ (signal);
if (DEV) {
if (current_async_effect) {
var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
var was_read = current_async_effect.deps?.includes(signal);
if (is_dirty(derived)) {
update_derived(derived);
if (!tracking && !was_read) {
w.await_reactivity_loss(/** @type {string} */ (signal.label));
var trace = get_stack('TracedAt');
// eslint-disable-next-line no-console
if (trace) console.warn(trace);
}
}
}
if (
DEV &&
tracing_mode_flag &&
!untracking &&
tracing_expressions !== null &&
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
if (signal.trace) {
signal.trace();
} else {
var trace = get_stack('TracedAt');
recent_async_deriveds.delete(signal);
if (trace) {
var entry = tracing_expressions.entries.get(signal);
if (
tracing_mode_flag &&
!untracking &&
tracing_expressions !== null &&
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
if (signal.trace) {
signal.trace();
} else {
trace = get_stack('TracedAt');
if (entry === undefined) {
entry = { traces: [] };
tracing_expressions.entries.set(signal, entry);
}
if (trace) {
var entry = tracing_expressions.entries.get(signal);
if (entry === undefined) {
entry = { traces: [] };
tracing_expressions.entries.set(signal, entry);
}
var last = entry.traces[entry.traces.length - 1];
var last = entry.traces[entry.traces.length - 1];
// traces can be duplicated, e.g. by `snapshot` invoking both
// both `getOwnPropertyDescriptor` and `get` traps at once
if (trace.stack !== last?.stack) {
entry.traces.push(trace);
// traces can be duplicated, e.g. by `snapshot` invoking both
// both `getOwnPropertyDescriptor` and `get` traps at once
if (trace.stack !== last?.stack) {
entry.traces.push(trace);
}
}
}
}
@ -859,6 +671,20 @@ export function get(signal) {
return value;
}
} else if (is_derived) {
derived = /** @type {Derived} */ (signal);
if (batch_deriveds?.has(derived)) {
return batch_deriveds.get(derived);
}
if (is_dirty(derived)) {
update_derived(derived);
}
}
if ((signal.f & ERROR_VALUE) !== 0) {
throw signal.v;
}
return signal.v;

@ -43,9 +43,7 @@ export type ComponentContext = {
m: Array<() => any>;
};
/** `$:` statements */
r1: any[];
/** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */
r2: Source<boolean>;
$: any[];
};
/**
* dev mode only: the component function

@ -18,6 +18,31 @@ export function assignment_value_stale(property, location) {
}
}
/**
* Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
* @param {string} name
*/
export function await_reactivity_loss(name) {
if (DEV) {
console.warn(`%c[svelte] await_reactivity_loss\n%cDetected reactivity loss when reading \`${name}\`. This happens when state is read in an async function after an earlier \`await\`\nhttps://svelte.dev/e/await_reactivity_loss`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/await_reactivity_loss`);
}
}
/**
* An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
* @param {string} name
* @param {string} location
*/
export function await_waterfall(name, location) {
if (DEV) {
console.warn(`%c[svelte] await_waterfall\n%cAn async derived, \`${name}\` (${location}) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app\nhttps://svelte.dev/e/await_waterfall`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/await_waterfall`);
}
}
/**
* `%binding%` (%location%) is binding to a non-reactive property
* @param {string} binding

@ -0,0 +1,3 @@
import { enable_async_mode_flag } from './index.js';
enable_async_mode_flag();

@ -1,6 +1,16 @@
export let async_mode_flag = false;
export let legacy_mode_flag = false;
export let tracing_mode_flag = false;
export function enable_async_mode_flag() {
async_mode_flag = true;
}
/** ONLY USE THIS DURING TESTING */
export function disable_async_mode_flag() {
async_mode_flag = false;
}
export function enable_legacy_mode_flag() {
legacy_mode_flag = true;
}

@ -520,6 +520,8 @@ export {
export { escape_html as escape };
export { await_outside_boundary } from '../shared/errors.js';
/**
* @template T
* @param {()=>T} fn

@ -2,6 +2,22 @@
import { DEV } from 'esm-env';
/**
* Cannot await outside a `<svelte:boundary>` with a `pending` snippet
* @returns {never}
*/
export function await_outside_boundary() {
if (DEV) {
const error = new Error(`await_outside_boundary\nCannot await outside a \`<svelte:boundary>\` with a \`pending\` snippet\nhttps://svelte.dev/e/await_outside_boundary`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/await_outside_boundary`);
}
}
/**
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}

@ -3,13 +3,15 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js';
import { active_effect, get, set_signal_status } from '../internal/client/runtime.js';
import { flushSync } from '../internal/client/reactivity/batch.js';
import { define_property, is_array } from '../internal/shared/utils.js';
import * as e from '../internal/client/errors.js';
import * as w from '../internal/client/warnings.js';
import { DEV } from 'esm-env';
import { FILENAME } from '../constants.js';
import { component_context, dev_current_component_function } from '../internal/client/context.js';
import { async_mode_flag } from '../internal/flags/index.js';
/**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
@ -119,8 +121,9 @@ class Svelte4Component {
recover: options.recover
});
// We don't flushSync for custom element wrappers or if the user doesn't want it
if (!options?.props?.$$host || options.sync === false) {
// We don't flushSync for custom element wrappers or if the user doesn't want it,
// or if we're in async mode since `flushSync()` will fail
if (!async_mode_flag && (!options?.props?.$$host || options.sync === false)) {
flushSync();
}

@ -3,6 +3,7 @@ import { effect_tracking, render_effect } from '../internal/client/reactivity/ef
import { source, increment } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { DEV } from 'esm-env';
import { queue_micro_task } from '../internal/client/dom/task.js';
/**
* Returns a `subscribe` function that, if called in an effect (including expressions in the template),
@ -68,8 +69,8 @@ export function createSubscriber(start) {
subscribers += 1;
return () => {
tick().then(() => {
// Only count down after timeout, else we would reach 0 before our own render effect reruns,
queue_micro_task(() => {
// Only count down after a microtask, else we would reach 0 before our own render effect reruns,
// but reach 1 again when the tick callback of the prior teardown runs. That would mean we
// re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
subscribers -= 1;

@ -445,6 +445,7 @@ const RUNES = /** @type {const} */ ([
'$effect.pre',
'$effect.tracking',
'$effect.root',
'$effect.pending',
'$inspect',
'$inspect().with',
'$inspect.trace',

@ -86,7 +86,8 @@ export async function compile_directory(
const compiled = compileModule(text, {
filename: opts.filename,
generate: opts.generate,
dev: opts.dev
dev: opts.dev,
experimental: opts.experimental
});
write(out, compiled.js.code.replace(`v${VERSION}`, 'VERSION'));
} else {
@ -193,6 +194,8 @@ if (typeof window !== 'undefined') {
export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html';
export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
/**
* @param {any[]} logs
*/

@ -27,6 +27,8 @@ export default test({
array: ['a', 'b', 'c']
});
raf.tick(25);
raf.tick(50);
assert.htmlEqual(
target.innerHTML,

@ -3,14 +3,15 @@ import { setImmediate } from 'node:timers/promises';
import { globSync } from 'tinyglobby';
import { createClassComponent } from 'svelte/legacy';
import { proxy } from 'svelte/internal/client';
import { flushSync, hydrate, mount, unmount, untrack } from 'svelte';
import { flushSync, hydrate, mount, unmount } from 'svelte';
import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory, fragments } from '../helpers.js';
import { async_mode, compile_directory, fragments } from '../helpers.js';
import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
import { clear } from '../../src/internal/client/reactivity/batch.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
@ -25,12 +26,30 @@ type Assert = typeof import('vitest').assert & {
): void;
};
// TODO remove this shim when we can
// @ts-expect-error
Promise.withResolvers = () => {
let resolve;
let reject;
const promise = new Promise((f, r) => {
resolve = f;
reject = r;
});
return { promise, resolve, reject };
};
export interface RuntimeTest<Props extends Record<string, any> = Record<string, any>>
extends BaseTest {
/** Use e.g. `mode: ['client']` to indicate that this test should never run in server/hydrate modes */
mode?: Array<'server' | 'client' | 'hydrate'>;
/** Temporarily skip specific modes, without skipping the entire test */
skip_mode?: Array<'server' | 'client' | 'hydrate'>;
/** Skip if running with process.env.NO_ASYNC */
skip_no_async?: boolean;
/** Skip if running without process.env.NO_ASYNC */
skip_async?: boolean;
html?: string;
ssrHtml?: string;
compileOptions?: Partial<CompileOptions>;
@ -107,7 +126,15 @@ let console_error = console.error;
export function runtime_suite(runes: boolean) {
return suite_with_variants<RuntimeTest, 'hydrate' | 'ssr' | 'dom', CompileOptions>(
['dom', 'hydrate', 'ssr'],
(variant, config) => {
(variant, config, test_name) => {
if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) {
return true;
}
if (async_mode && config.skip_async) {
return true;
}
if (variant === 'hydrate') {
if (config.mode && !config.mode.includes('hydrate')) return 'no-test';
if (config.skip_mode?.includes('hydrate')) return true;
@ -154,6 +181,9 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
rootDir: cwd,
dev: force_hmr ? true : undefined,
hmr: force_hmr ? true : undefined,
experimental: {
async: runes && async_mode
},
fragments,
...config.compileOptions,
immutable: config.immutable,
@ -215,7 +245,7 @@ async function run_test_variant(
if (str.slice(0, i).includes('warnings') || config.warnings) {
// eslint-disable-next-line no-console
console.warn = (...args) => {
if (args[0].startsWith('%c[svelte]')) {
if (typeof args[0] === 'string' && args[0].startsWith('%c[svelte]')) {
// TODO convert this to structured data, for more robust comparison?
let message = args[0];
@ -397,6 +427,12 @@ async function run_test_variant(
try {
if (config.test) {
flushSync();
if (variant === 'hydrate' && cwd.includes('async-')) {
// wait for pending boundaries to render
await Promise.resolve();
}
await config.test({
// @ts-expect-error TS doesn't get it
assert: {
@ -486,6 +522,8 @@ async function run_test_variant(
console.log = console_log;
console.warn = console_warn;
console.error = console_error;
clear();
}
}

@ -0,0 +1,23 @@
import { settled, tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [reset, resolve] = target.querySelectorAll('button');
reset.click();
await settled();
assert.deepEqual(logs, ['aborted']);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>reset</button>
<button>resolve</button>
<h1>hello</h1>
`
);
}
});

@ -0,0 +1,29 @@
<script>
import { getAbortSignal } from 'svelte';
let deferred = $state(Promise.withResolvers());
function load(deferred) {
const signal = getAbortSignal();
return new Promise((fulfil, reject) => {
signal.onabort = (e) => {
console.log('aborted');
reject(e.currentTarget.reason);
};
deferred.promise.then(fulfil, reject);
});
}
</script>
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
<button onclick={() => deferred.resolve('hello')}>resolve</button>
<svelte:boundary>
<h1>{await load(deferred)}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,18 @@
import { flushSync, tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<p>pending</p>
`,
async test({ assert, target }) {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
flushSync();
assert.htmlEqual(target.innerHTML, '<p data-foo="bar">hello</p>');
}
});

@ -0,0 +1,7 @@
<svelte:boundary>
<p data-foo={await 'bar'}>hello</p>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: `
<button>cool</button>
<button>neat</button>
<button>reset</button>
<p>pending</p>
`,
async test({ assert, target }) {
const [cool, neat, reset] = target.querySelectorAll('button');
cool.click();
await tick();
const p = target.querySelector('p');
ok(p);
assert.htmlEqual(p.outerHTML, '<p class="cool">hello</p>');
reset.click();
assert.htmlEqual(p.outerHTML, '<p class="cool">hello</p>');
neat.click();
await tick();
assert.htmlEqual(p.outerHTML, '<p class="neat">hello</p>');
}
});

@ -0,0 +1,15 @@
<script>
let deferred = $state(Promise.withResolvers());
</script>
<button onclick={() => deferred.resolve('cool')}>cool</button>
<button onclick={() => deferred.resolve('neat')}>neat</button>
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
<svelte:boundary>
<p class={await deferred.promise}>hello</p>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,27 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>false</p>
<p>1</p>
`
);
}
});

@ -0,0 +1,28 @@
<script>
let count = $state(0);
let deferreds = [];
function push() {
const deferred = Promise.withResolvers();
deferreds.push(deferred);
return deferred.promise;
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => deferreds.shift()?.resolve(count)}>shift</button>
<svelte:boundary>
{#if count % 2 === 0}
<p>true</p>
<p>{await push()}</p>
{:else}
<p>false</p>
<p>{await push()}</p>
{/if}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, 'loading');
await tick();
assert.htmlEqual(target.innerHTML, 'nope');
}
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save