feat: `$state.eager(value)` (#16926)

* runtime-first approach

* revert these

* type safety, lint

* fix: better input cursor restoration for `bind:value` (#16925)

If cursor was at end and new input is longer, move cursor to new end

No test because not possible to reproduce using our test setup.

Follow-up to #14649, helps with #16577

* Version Packages (#16920)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs: await no longer need pending (#16900)

* docs: link to custom renderer issue in Svelte Native discussion (#16896)

* fix code block (#16937)

Updated code block syntax from Svelte to JavaScript for clarity.

* fix: unset context on stale promises (#16935)

* fix: unset context on stale promises

When a stale promise is rejected in `async_derived`, and the promise eventually resolves, `d.resolve` will be noop and `d.promise.then(handler, ...)` will never run. That in turns means any restored context (via `(await save(..))()`) will never be unset. We have to handle this case and unset the context to prevent errors such as false-positive state mutation errors

* fix: unset context on stale promises (slightly different approach) (#16936)

* slightly different approach to #16935

* move unset_context call

* get rid of logs

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* fix: svg `radialGradient` `fr` attribute missing in types (#16943)

* fix(svg radialGradient): fr attribute missing in types

* chore: add changeset

* Version Packages (#16940)

* Version Packages

* Update packages/svelte/CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>

* chore: simplify `batch.apply()` (#16945)

* chore: simplify `batch.apply()`

* belt and braces

* note to self

* unused

* fix: don't rerun async effects unnecessarily (#16944)

Since #16866, when an async effect runs multiple times, we rebase older batches and rerun those effects. This can have unintended consequences: In a case where an async effect only depends on a single source, and that single source was updated in a later batch, we know that we don't need to / should not rerun the older batch.

This PR makes it so: We collect all the sources of older batches that are not part of the current batch that just committed, and then only mark those async effects as dirty which depend on one of those other sources. Fixes the bug I noticed while working on #16935

* fix: ensure map iteration order is correct (#16947)

quick follow-up to #16944
Resetting a map entry does not change its position in the map when iterating. We need to make sure that reset makes that batch jump "to the front" for the "reject all stale batches" logic below. Edge case for which I can't come up with a test case but it _is_ a possibility.

* feat: add `createContext` utility for type-safe context (#16948)

* feat: add `createContext` utility for type-safe context

* regenerate

* Version Packages (#16946)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore: Remove annoying sync-async warning (#16949)

* fix

* use `$state.eager(value)` instead of `$effect.pending(value)`

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hyunbin Seo <47051820+hyunbinseo@users.noreply.github.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Hannes Rüger <hannes@hannesrueger.de>
Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
state-eager-derived-block
Simon H 1 month ago committed by GitHub
parent f38319aec7
commit d8e61f6651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: Remove sync-in-async warning for server rendering

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: track the user's getter of `bind:this`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: generate correct SSR code for the case where `pending` is an attribute

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: generate correct code for `each` blocks with async body

@ -85,7 +85,7 @@ Derived expressions are recalculated when their dependencies change, but you can
Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
```svelte
```js
let items = $state([...]);
let index = $state(0);

@ -24,7 +24,7 @@ For the boundary to do anything, one or more of the following must be provided.
### `pending`
As of Svelte 5.36, boundaries with a `pending` snippet can contain [`await`](await-expressions) expressions. This snippet will be shown when the boundary is first created, and will remain visible until all the `await` expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
This snippet will be shown when the boundary is first created, and will remain visible until all the [`await`](await-expressions) expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
```svelte
<svelte:boundary>

@ -99,7 +99,7 @@ However, you can use any router library. A sampling of available routers are hig
While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms.
Svelte Native was an option available for Svelte 4, but note that Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native.
Some work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet available. The custom rendering API would support additional mobile frameworks like Lynx JS and Svelte Native. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native.
## Can I tell Svelte not to remove my unused styles?

@ -1,9 +0,0 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### experimental_async_ssr
```
Attempted to use asynchronous rendering without `experimental.async` enabled
```
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -60,6 +60,14 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
### missing_context
```
Context was not set in a parent component
```
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component.
### snippet_without_render_tag
```

@ -1,5 +1,37 @@
# svelte
## 5.40.0
### Minor Changes
- feat: add `createContext` utility for type-safe context ([#16948](https://github.com/sveltejs/svelte/pull/16948))
### Patch Changes
- chore: simplify `batch.apply()` ([#16945](https://github.com/sveltejs/svelte/pull/16945))
- fix: don't rerun async effects unnecessarily ([#16944](https://github.com/sveltejs/svelte/pull/16944))
## 5.39.13
### Patch Changes
- fix: add missing type for `fr` attribute for `radialGradient` tags in svg ([#16943](https://github.com/sveltejs/svelte/pull/16943))
- fix: unset context on stale promises ([#16935](https://github.com/sveltejs/svelte/pull/16935))
## 5.39.12
### Patch Changes
- fix: better input cursor restoration for `bind:value` ([#16925](https://github.com/sveltejs/svelte/pull/16925))
- fix: track the user's getter of `bind:this` ([#16916](https://github.com/sveltejs/svelte/pull/16916))
- fix: generate correct SSR code for the case where `pending` is an attribute ([#16919](https://github.com/sveltejs/svelte/pull/16919))
- fix: generate correct code for `each` blocks with async body ([#16923](https://github.com/sveltejs/svelte/pull/16923))
## 5.39.11
### Patch Changes

@ -1658,6 +1658,7 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
'font-variant'?: number | string | undefined | null;
'font-weight'?: number | string | undefined | null;
format?: number | string | undefined | null;
fr?: number | string | undefined | null;
from?: number | string | undefined | null;
fx?: number | string | undefined | null;
fy?: number | string | undefined | null;

@ -1,5 +0,0 @@
## experimental_async_ssr
> Attempted to use asynchronous rendering without `experimental.async` enabled
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -52,6 +52,12 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
## missing_context
> Context was not set in a parent component
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component.
## snippet_without_render_tag
> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.39.11",
"version": "5.40.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -95,12 +95,24 @@ declare namespace $state {
: never
: never;
/**
* Returns the latest `value`, even if the rest of the UI is suspending
* while async work (such as data loading) completes.
*
* ```svelte
* <nav>
* <a href="/" aria-current={$state.eager(href) === '/' ? 'page' : null}>home</a>
* <a href="/about" aria-current={$state.eager(href) === '/about' ? 'page' : null}>about</a>
* </nav>
* ```
*/
export function eager<T>(value: T): T;
/**
* Declares state that is _not_ made deeply reactive instead of mutating it,
* you must reassign it.
*
* Example:
* ```ts
* ```svelte
* <script>
* let items = $state.raw([0]);
*
@ -109,7 +121,7 @@ declare namespace $state {
* };
* </script>
*
* <button on:click={addItem}>
* <button onclick={addItem}>
* {items.join(', ')}
* </button>
* ```
@ -124,7 +136,7 @@ declare namespace $state {
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* ```svelte
* <script>
* let counter = $state({ count: 0 });
*

@ -11,8 +11,7 @@ export function create_fragment(transparent = false) {
metadata: {
transparent,
dynamic: false,
has_await: false,
effect_pending_expressions: []
has_await: false
}
};
}

@ -165,22 +165,10 @@ export function CallExpression(node, context) {
break;
case '$effect.pending':
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
if (context.state.expression) {
context.state.expression.has_state = true;
}
if (node.arguments[0]) {
const fragment = /** @type {AST.Fragment} */ (context.state.fragment);
fragment.metadata.effect_pending_expressions.push(
/** @type {Expression} */ (node.arguments[0])
);
}
break;
case '$inspect':
@ -238,6 +226,13 @@ export function CallExpression(node, context) {
break;
}
case '$state.eager':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');

@ -24,8 +24,6 @@ export interface ClientTransformState extends TransformState {
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
effect_pending: Map<Expression, Identifier>;
readonly transform: Record<
string,
{

@ -49,6 +49,12 @@ export function CallExpression(node, context) {
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
case '$state.eager':
return b.call(
'$.pending',
b.thunk(/** @type {Expression} */ (context.visit(node.arguments[0])))
);
case '$state.snapshot':
return b.call(
'$.snapshot',
@ -63,17 +69,6 @@ export function CallExpression(node, context) {
);
case '$effect.pending':
if (node.arguments[0]) {
const id = b.id(`$$pending_${context.state.effect_pending.size}`);
context.state.effect_pending.set(
/** @type {Expression} */ (context.visit(node.arguments[0])),
id
);
return b.call('$.get', id);
}
return b.call('$.pending');
case '$inspect':

@ -68,7 +68,6 @@ export function Fragment(node, context) {
memoizer: new Memoizer(),
template: new Template(),
transform: { ...context.state.transform },
effect_pending: new Map(),
metadata: {
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
@ -153,10 +152,6 @@ export function Fragment(node, context) {
body.push(...state.consts);
for (const [expression, id] of state.effect_pending) {
body.push(b.const(id, b.call('$.pending', b.thunk(expression))));
}
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
}

@ -310,8 +310,7 @@ export function RegularElement(node, context) {
metadata,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
effect_pending: new Map()
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
};
const { hoisted, trimmed } = clean_nodes(
@ -379,10 +378,6 @@ export function RegularElement(node, context) {
}
}
for (const [expression, id] of state.effect_pending) {
child_state.init.push(b.const(id, b.call('$.pending', b.thunk(expression))));
}
if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock')) {
// Wrap children in `{...}` to avoid declaration conflicts
context.state.init.push(

@ -26,7 +26,7 @@ export function CallExpression(node, context) {
}
if (rune === '$effect.pending') {
return node.arguments[0] ?? b.literal(0);
return b.literal(0);
}
if (rune === '$state' || rune === '$state.raw') {
@ -38,6 +38,10 @@ export function CallExpression(node, context) {
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
if (rune === '$state.eager') {
return node.arguments[0];
}
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',

@ -57,7 +57,6 @@ export namespace AST {
*/
dynamic: boolean;
has_await: boolean;
effect_pending_expressions: Expression[];
};
}

@ -242,7 +242,13 @@ function init_update_callbacks(context) {
}
export { flushSync } from './internal/client/reactivity/batch.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export {
createContext,
getContext,
getAllContexts,
hasContext,
setContext
} from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -39,6 +39,12 @@ export async function settled() {}
export { getAbortSignal } from './internal/server/abort-signal.js';
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export {
createContext,
getAllContexts,
getContext,
hasContext,
setContext
} from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';

@ -69,6 +69,26 @@ export function set_dev_current_component_function(fn) {
dev_current_component_function = fn;
}
/**
* Returns a `[get, set]` pair of functions for working with context in a type-safe way.
* @template T
* @returns {[() => T, (context: T) => T]}
*/
export function createContext() {
const key = {};
return [
() => {
if (!hasContext(key)) {
e.missing_context();
}
return getContext(key);
},
(context) => setContext(key, context)
];
}
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.

@ -8,19 +8,15 @@ import {
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import {
block,
branch,
destroy_effect,
inspect_effect,
pause_effect
} from '../../reactivity/effects.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
get,
read_pending,
set_active_effect,
set_active_reaction
set_active_reaction,
set_read_pending
} from '../../runtime.js';
import {
hydrate_next,
@ -35,11 +31,10 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, current_batch, effect_pending_updates } from '../../reactivity/batch.js';
import { Batch, effect_pending_updates } 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';
import { untrack } from 'svelte';
/**
* @typedef {{
@ -455,7 +450,7 @@ export function get_boundary() {
}
/**
* @param {(() => any) | void} fn
* @param {() => any} [fn]
*/
export function pending(fn) {
if (active_effect === null) {
@ -465,28 +460,23 @@ export function pending(fn) {
var boundary = active_effect.b;
if (boundary === null) {
return 0; // TODO eventually we will need this to be global
return fn ? fn() : 0; // TODO eventually we will need this to be global
}
if (fn) {
const signal = source(untrack(fn));
inspect_effect(() => {
const value = fn();
let stale = false;
queue_micro_task(() => {
if (stale) return;
internal_set(signal, value);
});
var pending = boundary.get_effect_pending();
return () => {
stale = true;
};
});
if (fn) {
var value;
var prev_read_pending = read_pending;
set_read_pending(true);
try {
value = fn();
} finally {
set_read_pending(prev_read_pending);
}
return signal;
return value;
} else {
return pending;
}
return boundary.get_effect_pending();
}

@ -43,14 +43,22 @@ export function bind_value(input, get, set = get) {
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;
var length = input.value.length;
// the value is coerced on assignment
input.value = value ?? '';
// Restore selection
if (end !== null) {
input.selectionStart = start;
input.selectionEnd = Math.min(end, input.value.length);
var new_length = input.value.length;
// If cursor was at end and new input is longer, move cursor to new end
if (start === end && end === length && new_length > length) {
input.selectionStart = new_length;
input.selectionEnd = new_length;
} else {
input.selectionStart = start;
input.selectionEnd = Math.min(end, new_length);
}
}
}
});

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Source, Value } from '#client' */
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
@ -14,7 +14,7 @@ import {
DERIVED
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, noop } from '../../shared/utils.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
is_dirty,
@ -44,12 +44,12 @@ export let current_batch = null;
export let previous_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}
* When time travelling (i.e. working in one batch, while other batches
* still have ongoing work), we ignore the real values of affected
* signals in favour of their values within the batch
* @type {Map<Value, any> | null}
*/
export let batch_deriveds = null;
export let batch_values = null;
/** @type {Set<() => void>} */
export let effect_pending_updates = new Set();
@ -152,7 +152,7 @@ export class Batch {
previous_batch = null;
var revert = Batch.apply(this);
this.apply();
for (const root of root_effects) {
this.#traverse_effect_tree(root);
@ -161,6 +161,10 @@ export class Batch {
// if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit
if (this.#pending === 0) {
// TODO we need this because we commit _then_ flush effects...
// maybe there's a way we can reverse the order?
var previous_batch_sources = batch_values;
this.#commit();
var render_effects = this.#render_effects;
@ -175,6 +179,7 @@ export class Batch {
previous_batch = this;
current_batch = null;
batch_values = previous_batch_sources;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
@ -187,7 +192,7 @@ export class Batch {
this.#defer_effects(this.#block_effects);
}
revert();
batch_values = null;
for (const effect of this.#boundary_async_effects) {
update_effect(effect);
@ -274,6 +279,7 @@ export class Batch {
}
this.current.set(source, source.v);
batch_values?.set(source, source.v);
}
activate() {
@ -282,6 +288,7 @@ export class Batch {
deactivate() {
current_batch = null;
batch_values = null;
}
flush() {
@ -335,31 +342,46 @@ export class Batch {
continue;
}
/** @type {Source[]} */
const sources = [];
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
if (is_earlier) {
if (is_earlier && value !== batch.current.get(source)) {
// bring the value up to date
batch.current.set(source, value);
} else {
// later batch has more recent value,
// same value or later batch has more recent value,
// no need to re-run these effects
continue;
}
}
mark_effects(source);
sources.push(source);
}
if (queued_root_effects.length > 0) {
current_batch = batch;
const revert = Batch.apply(batch);
if (sources.length === 0) {
continue;
}
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root);
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
for (const source of sources) {
mark_effects(source, others);
}
queued_root_effects = [];
revert();
if (queued_root_effects.length > 0) {
current_batch = batch;
batch.apply();
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root);
}
queued_root_effects = [];
batch.deactivate();
}
}
}
@ -423,49 +445,23 @@ export class Batch {
queue_micro_task(task);
}
/**
* @param {Batch} current_batch
*/
static apply(current_batch) {
if (!async_mode_flag || batches.size === 1) {
return noop;
}
apply() {
if (!async_mode_flag || batches.size === 1) return;
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
/** @type {Map<Source, { v: unknown, wv: number }>} */
var current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of current_batch.current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
// we need to override values with the ones in this batch...
batch_values = new Map(this.current);
// ...and undo changes belonging to other batches
for (const batch of batches) {
if (batch === current_batch) continue;
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;
if (!batch_values.has(source)) {
batch_values.set(source, previous);
}
}
}
return () => {
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;
};
}
}
@ -640,17 +636,19 @@ function flush_queued_effects(effects) {
/**
* This is similar to `mark_reactions`, but it only marks async/block effects
* so that these can re-run after another batch has been committed
* depending on `value` and at least one of the other `sources`, so that
* these effects can re-run after another batch has been committed
* @param {Value} value
* @param {Source[]} sources
*/
function mark_effects(value) {
function mark_effects(value, sources) {
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_effects(/** @type {Derived} */ (reaction));
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) {
mark_effects(/** @type {Derived} */ (reaction), sources);
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
@ -658,6 +656,26 @@ function mark_effects(value) {
}
}
/**
* @param {Reaction} reaction
* @param {Source[]} sources
*/
function depends_on(reaction, sources) {
if (reaction.deps !== null) {
for (const dep of reaction.deps) {
if (sources.includes(dep)) {
return true;
}
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) {
return true;
}
}
}
return false;
}
/**
* @param {Effect} signal
* @returns {void}

@ -33,7 +33,7 @@ import { async_mode_flag, 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 { batch_values, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred } from '../../shared/utils.js';
@ -126,9 +126,11 @@ export function async_derived(fn, location) {
try {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
Promise.resolve(fn()).then(d.resolve, d.reject);
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context);
} catch (error) {
d.reject(error);
unset_context();
}
if (DEV) current_async_effect = null;
@ -142,6 +144,7 @@ export function async_derived(fn, location) {
batch.increment();
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
deferreds.set(batch, d);
}
}
@ -169,6 +172,13 @@ export function async_derived(fn, location) {
internal_set(signal, value);
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
deferreds.delete(b);
if (b === batch) break;
d.reject(STALE_REACTION);
}
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
@ -185,8 +195,6 @@ export function async_derived(fn, location) {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
}
unset_context();
};
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
@ -336,6 +344,8 @@ export function update_derived(derived) {
var value = execute_derived(derived);
if (!derived.equals(value)) {
// TODO can we avoid setting `derived.v` when `batch_values !== null`,
// without causing the value to be stale later?
derived.v = value;
derived.wv = increment_write_version();
}
@ -346,8 +356,8 @@ export function update_derived(derived) {
return;
}
if (batch_deriveds !== null) {
batch_deriveds.set(derived, derived.v);
if (batch_values !== null) {
batch_values.set(derived, derived.v);
} else {
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;

@ -42,7 +42,7 @@ import {
set_dev_stack
} from './context.js';
import * as w from './warnings.js';
import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
@ -144,6 +144,17 @@ export function increment_write_version() {
return ++write_version;
}
/**
* Whether or not we should get the latest value of a signal regardless of whether or not it is pending,
* i.e. inside a boundary with pending async work in which case normally a stale value might be shown.
*/
export let read_pending = false;
/** @param {boolean} value */
export function set_read_pending(value) {
read_pending = value;
}
/**
* Determines whether a derived or effect is dirty.
* If it is MAYBE_DIRTY, will set the status to CLEAN
@ -653,7 +664,7 @@ export function get(signal) {
if (is_derived) {
derived = /** @type {Derived} */ (signal);
var value = derived.v;
var derived_value = derived.v;
// if the derived is dirty and has reactions, or depends on the values that just changed, re-execute
// (a derived can be maybe_dirty due to the effect destroy removing its last reaction)
@ -661,18 +672,18 @@ export function get(signal) {
((derived.f & CLEAN) === 0 && derived.reactions !== null) ||
depends_on_old_values(derived)
) {
value = execute_derived(derived);
derived_value = execute_derived(derived);
}
old_values.set(derived, value);
old_values.set(derived, derived_value);
return value;
return derived_value;
}
} else if (is_derived) {
derived = /** @type {Derived} */ (signal);
if (batch_deriveds?.has(derived)) {
return batch_deriveds.get(derived);
if (batch_values?.has(derived)) {
return batch_values.get(derived);
}
if (is_dirty(derived)) {
@ -680,6 +691,10 @@ export function get(signal) {
}
}
if (!read_pending && batch_values?.has(signal)) {
return batch_values.get(signal);
}
if ((signal.f & ERROR_VALUE) !== 0) {
throw signal.v;
}

@ -10,6 +10,15 @@ export function set_ssr_context(v) {
ssr_context = v;
}
/**
* @template T
* @returns {[() => T, (context: T) => T]}
*/
export function createContext() {
const key = {};
return [() => getContext(key), (context) => setContext(key, context)];
}
/**
* @template T
* @param {any} key

@ -4,7 +4,6 @@ import { async_mode_flag } from '../flags/index.js';
import { abort } from './abort-signal.js';
import { pop, push, set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import * as w from './warnings.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { attributes } from './index.js';
@ -361,7 +360,6 @@ export class Renderer {
*/
(onfulfilled, onrejected) => {
if (!async_mode_flag) {
w.experimental_async_ssr();
const result = (sync ??= Renderer.#render(component, options));
const user_result = onfulfilled({
head: result.head,

@ -3,15 +3,4 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* Attempted to use asynchronous rendering without `experimental.async` enabled
*/
export function experimental_async_ssr() {
if (DEV) {
console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/experimental_async_ssr`);
}
}
var normal = 'font-weight: normal';

@ -51,6 +51,22 @@ export function lifecycle_outside_component(name) {
}
}
/**
* Context was not set in a parent component
* @returns {never}
*/
export function missing_context() {
if (DEV) {
const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/missing_context`);
}
}
/**
* Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
* @returns {never}

@ -53,7 +53,6 @@ export function asClassComponent(component) {
*/
value: (onfulfilled, onrejected) => {
if (!async_mode_flag) {
w.experimental_async_ssr();
const user_result = onfulfilled({
css: munged.css,
head: munged.head,

@ -436,6 +436,7 @@ const STATE_CREATION_RUNES = /** @type {const} */ ([
const RUNES = /** @type {const} */ ([
...STATE_CREATION_RUNES,
'$state.eager',
'$state.snapshot',
'$props',
'$props.id',

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.39.11';
export const VERSION = '5.40.0';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,25 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
// We gotta wait a bit more in this test because of the macrotasks in App.svelte
function macrotask(t = 3) {
return new Promise((r) => setTimeout(r, t));
}
await macrotask();
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
const [input] = target.querySelectorAll('input');
input.value = '1';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask();
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
input.value = '12';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask(6);
assert.htmlEqual(target.innerHTML, '<input> 3 | 12');
}
});

@ -0,0 +1,34 @@
<script>
let count = $state(0);
let value = $state('');
let prev;
function asd(v) {
const r = Promise.withResolvers();
if (prev || v === '') {
Promise.resolve().then(async () => {
count++;
r.resolve(v);
await new Promise(r => setTimeout(r, 0));
// TODO with a microtask like below it still throws a mutation error
// await Promise.resolve();
prev?.resolve();
});
} else {
prev = Promise.withResolvers();
prev.promise.then(() => {
count++;
r.resolve(v)
});
}
return r.promise;
}
const x = $derived(await asd(value))
</script>
<input bind:value />
{count} | {x}

@ -0,0 +1,7 @@
<script>
import { get } from './main.svelte';
const message = get();
</script>
<h1>{message}</h1>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<h1>hello</h1>`
});

@ -0,0 +1,16 @@
<script module>
import { createContext } from 'svelte';
/** @type {ReturnType<typeof createContext<string>>} */
const [get, set] = createContext();
export { get };
</script>
<script>
import Child from './Child.svelte';
set('hello');
</script>
<Child />

@ -448,6 +448,10 @@ declare module 'svelte' {
}): Snippet<Params>;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
* Returns a `[get, set]` pair of functions for working with context in a type-safe way.
* */
export function createContext<T>(): [() => T, (context: T) => T];
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
@ -3181,12 +3185,24 @@ declare namespace $state {
: never
: never;
/**
* Returns the latest `value`, even if the rest of the UI is suspending
* while async work (such as data loading) completes.
*
* ```svelte
* <nav>
* <a href="/" aria-current={$state.eager(href) === '/' ? 'page' : null}>home</a>
* <a href="/about" aria-current={$state.eager(href) === '/about' ? 'page' : null}>about</a>
* </nav>
* ```
*/
export function eager<T>(value: T): T;
/**
* Declares state that is _not_ made deeply reactive instead of mutating it,
* you must reassign it.
*
* Example:
* ```ts
* ```svelte
* <script>
* let items = $state.raw([0]);
*
@ -3195,7 +3211,7 @@ declare namespace $state {
* };
* </script>
*
* <button on:click={addItem}>
* <button onclick={addItem}>
* {items.join(', ')}
* </button>
* ```
@ -3210,7 +3226,7 @@ declare namespace $state {
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* ```svelte
* <script>
* let counter = $state({ count: 0 });
*

Loading…
Cancel
Save