diff --git a/.changeset/selfish-pets-teach.md b/.changeset/selfish-pets-teach.md
deleted file mode 100644
index d78fea8f9f..0000000000
--- a/.changeset/selfish-pets-teach.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-chore: run boundary async effects in the context of the current batch
diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index 741e24fde0..6fbf3b8895 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -166,6 +166,21 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
+## `$state.eager`
+
+When state changes, it may not be reflected in the UI immediately if it is used by an `await` expression, because [updates are synchronized](await-expressions#Synchronized-updates).
+
+In some cases, you may want to update the UI as soon as the state changes. For example, you might want to update a navigation bar when the user clicks on a link, so that they get visual feedback while waiting for the new page to load. To do this, use `$state.eager(value)`:
+
+```svelte
+
+ home
+ about
+
+```
+
+Use this feature sparingly, and only to provide feedback in response to user action — in general, allowing Svelte to coordinate updates will provide a better user experience.
+
## Passing state into functions
JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words:
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index b9c44163c9..c5703c636b 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -199,7 +199,7 @@ Cyclical dependency detected: %cycle%
### const_tag_invalid_reference
```
-The `{@const %name% = ...}` declaration is not available in this snippet
+The `{@const %name% = ...}` declaration is not available in this snippet
```
The following is an error:
@@ -453,6 +453,12 @@ This turned out to be buggy and unpredictable, particularly when working with de
{/each}
```
+### each_key_without_as
+
+```
+An `{#each ...}` block without an `as` clause cannot have a key
+```
+
### effect_invalid_placement
```
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index 5c8a5e5b58..4db131114d 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,29 @@
# svelte
+## 5.41.1
+
+### Patch Changes
+
+- fix: place `let:` declarations before `{@const}` declarations ([#16985](https://github.com/sveltejs/svelte/pull/16985))
+
+- fix: improve `each_key_without_as` error ([#16983](https://github.com/sveltejs/svelte/pull/16983))
+
+- chore: centralise branch management ([#16977](https://github.com/sveltejs/svelte/pull/16977))
+
+## 5.41.0
+
+### Minor Changes
+
+- feat: add `$state.eager(value)` rune ([#16849](https://github.com/sveltejs/svelte/pull/16849))
+
+### Patch Changes
+
+- fix: preserve `` state while focused ([#16958](https://github.com/sveltejs/svelte/pull/16958))
+
+- chore: run boundary async effects in the context of the current batch ([#16968](https://github.com/sveltejs/svelte/pull/16968))
+
+- fix: error if `each` block has `key` but no `as` clause ([#16966](https://github.com/sveltejs/svelte/pull/16966))
+
## 5.40.2
### Patch Changes
diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md
index dc26a02767..ac95bfe4a7 100644
--- a/packages/svelte/messages/compile-errors/template.md
+++ b/packages/svelte/messages/compile-errors/template.md
@@ -126,7 +126,7 @@
## const_tag_invalid_reference
-> The `{@const %name% = ...}` declaration is not available in this snippet
+> The `{@const %name% = ...}` declaration is not available in this snippet
The following is an error:
@@ -179,6 +179,10 @@ The same applies to components:
> `%type%` name cannot be empty
+## each_key_without_as
+
+> An `{#each ...}` block without an `as` clause cannot have a key
+
## element_invalid_closing_tag
> `%name%>` attempted to close an element that was not open
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index d99ddb502d..b5a20ce82a 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.40.2",
+ "version": "5.41.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index 1f1b0e7b5e..823dbde9a4 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -95,6 +95,18 @@ 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
+ *
+ * home
+ * about
+ *
+ * ```
+ */
+ export function eager(value: T): T;
/**
* Declares state that is _not_ made deeply reactive — instead of mutating it,
* you must reassign it.
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 44fc641ee5..5e3968215f 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -986,13 +986,13 @@ export function const_tag_invalid_placement(node) {
}
/**
- * The `{@const %name% = ...}` declaration is not available in this snippet
+ * The `{@const %name% = ...}` declaration is not available in this snippet
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function const_tag_invalid_reference(node, name) {
- e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`);
+ e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet\nhttps://svelte.dev/e/const_tag_invalid_reference`);
}
/**
@@ -1023,6 +1023,15 @@ export function directive_missing_name(node, type) {
e(node, 'directive_missing_name', `\`${type}\` name cannot be empty\nhttps://svelte.dev/e/directive_missing_name`);
}
+/**
+ * An `{#each ...}` block without an `as` clause cannot have a key
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function each_key_without_as(node) {
+ e(node, 'each_key_without_as', `An \`{#each ...}\` block without an \`as\` clause cannot have a key\nhttps://svelte.dev/e/each_key_without_as`);
+}
+
/**
* `%name%>` attempted to close an element that was not open
* @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 53a89125a2..76d9cecd9a 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -226,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');
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js
index e6a83921b1..81a9c1e2d1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js
@@ -1,3 +1,4 @@
+/** @import { Expression } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
@@ -28,6 +29,10 @@ export function EachBlock(node, context) {
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
+ if (node.metadata.keyed && !node.context) {
+ e.each_key_without_as(/** @type {Expression} */ (node.key));
+ }
+
// evaluate expression in parent scope
context.visit(node.expression, {
...context.state,
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index 2629379f63..0dd4ae03b9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -172,6 +172,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
+ let_directives: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
@@ -384,7 +385,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body)
]);
- component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
+ component_block.body.push(
+ b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true)))
+ );
} else {
component_block.body.push(
...state.instance_level_snippets,
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index 932d353671..b9a8691a6b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -54,6 +54,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
+ /** Transformed `let:` directives */
+ readonly let_directives: Statement[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index fcc385c2ba..bf9a09bb74 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -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(
+ '$.eager',
+ b.thunk(/** @type {Expression} */ (context.visit(node.arguments[0])))
+ );
+
case '$state.snapshot':
return b.call(
'$.snapshot',
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
index 85d8e3caff..8d6a2fac88 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
@@ -63,6 +63,7 @@ export function Fragment(node, context) {
...context.state,
init: [],
consts: [],
+ let_directives: [],
update: [],
after_update: [],
memoizer: new Memoizer(),
@@ -150,7 +151,7 @@ export function Fragment(node, context) {
}
}
- body.push(...state.consts);
+ body.push(...state.let_directives, ...state.consts);
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
@@ -177,7 +178,11 @@ export function Fragment(node, context) {
}
if (has_await) {
- return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]);
+ return b.block([
+ b.stmt(
+ b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
+ )
+ ]);
} else {
return b.block(body);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
index f33febeeb2..c134b4e1e7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
@@ -21,22 +21,24 @@ export function LetDirective(node, context) {
};
}
- return b.const(
- name,
- b.call(
- '$.derived',
- b.thunk(
- b.block([
- b.let(
- /** @type {Expression} */ (node.expression).type === 'ObjectExpression'
- ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
- b.object_pattern(node.expression.properties)
- : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
- b.array_pattern(node.expression.elements),
- b.member(b.id('$$slotProps'), node.name)
- ),
- b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
- ])
+ context.state.let_directives.push(
+ b.const(
+ name,
+ b.call(
+ '$.derived',
+ b.thunk(
+ b.block([
+ b.let(
+ /** @type {Expression} */ (node.expression).type === 'ObjectExpression'
+ ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
+ b.object_pattern(node.expression.properties)
+ : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
+ b.array_pattern(node.expression.elements),
+ b.member(b.id('$$slotProps'), node.name)
+ ),
+ b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
+ ])
+ )
)
)
);
@@ -46,6 +48,8 @@ export function LetDirective(node, context) {
read: (node) => b.call('$.get', node)
};
- return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)));
+ context.state.let_directives.push(
+ b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)))
+ );
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
index e35b7cbe5a..ab119e8f80 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
@@ -106,7 +106,7 @@ export function RegularElement(node, context) {
case 'LetDirective':
// visit let directives before everything else, to set state
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
+ context.visit(attribute, { ...context.state, let_directives: lets });
break;
case 'OnDirective':
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
index a5c0974738..b87a13253b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
@@ -49,7 +49,7 @@ export function SlotElement(node, context) {
}
}
} else if (attribute.type === 'LetDirective') {
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
+ context.visit(attribute, { ...context.state, let_directives: lets });
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js
index 65cc170ce5..e3b46a4eef 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js
@@ -9,7 +9,7 @@
export function SvelteFragment(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
- context.state.init.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
+ context.visit(attribute);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
index 5c8ce897f4..5ca941fd70 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
@@ -101,7 +101,7 @@ export function build_component(node, component_name, context) {
if (slot_scope_applies_to_itself) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
+ context.visit(attribute, { ...context.state, let_directives: lets });
}
}
}
@@ -109,7 +109,7 @@ export function build_component(node, component_name, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default)));
+ context.visit(attribute, { ...states.default, let_directives: lets });
}
} else if (attribute.type === 'OnDirective') {
if (!attribute.expression) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
index 41d3202ce9..d53b631aa5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
@@ -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',
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index e7917fbd9e..bac01e4c33 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -1,12 +1,9 @@
-/** @import { Effect, Source, TemplateNode } from '#client' */
-import { DEV } from 'esm-env';
+/** @import { Source, TemplateNode } from '#client' */
import { is_promise } from '../../../shared/utils.js';
-import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
+import { block } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
-import { set_active_effect, set_active_reaction } from '../../runtime.js';
import {
hydrate_next,
- hydrate_node,
hydrating,
skip_nodes,
set_hydrate_node,
@@ -14,15 +11,10 @@ import {
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
-import {
- component_context,
- dev_stack,
- is_runes,
- set_component_context,
- set_dev_current_component_function,
- set_dev_stack
-} from '../../context.js';
+import { is_runes } from '../../context.js';
import { flushSync, is_flushing_sync } from '../../reactivity/batch.js';
+import { BranchManager } from './branches.js';
+import { capture, unset_context } from '../../reactivity/async.js';
const PENDING = 0;
const THEN = 1;
@@ -33,7 +25,7 @@ const CATCH = 2;
/**
* @template V
* @param {TemplateNode} node
- * @param {(() => Promise)} get_input
+ * @param {(() => any)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
@@ -44,149 +36,94 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
hydrate_next();
}
- var anchor = node;
var runes = is_runes();
- var active_component_context = component_context;
-
- /** @type {any} */
- var component_function = DEV ? component_context?.function : null;
- var dev_original_stack = DEV ? dev_stack : null;
-
- /** @type {V | Promise | typeof UNINITIALIZED} */
- var input = UNINITIALIZED;
-
- /** @type {Effect | null} */
- var pending_effect;
-
- /** @type {Effect | null} */
- var then_effect;
-
- /** @type {Effect | null} */
- var catch_effect;
-
- var input_source = runes
- ? source(/** @type {V} */ (undefined))
- : mutable_source(/** @type {V} */ (undefined), false, false);
- var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
- var resolved = false;
- /**
- * @param {AwaitState} state
- * @param {boolean} restore
- */
- function update(state, restore) {
- resolved = true;
-
- if (restore) {
- set_active_effect(effect);
- set_active_reaction(effect); // TODO do we need both?
- set_component_context(active_component_context);
- if (DEV) {
- set_dev_current_component_function(component_function);
- set_dev_stack(dev_original_stack);
- }
- }
-
- try {
- if (state === PENDING && pending_fn) {
- if (pending_effect) resume_effect(pending_effect);
- else pending_effect = branch(() => pending_fn(anchor));
- }
-
- if (state === THEN && then_fn) {
- if (then_effect) resume_effect(then_effect);
- else then_effect = branch(() => then_fn(anchor, input_source));
- }
-
- if (state === CATCH && catch_fn) {
- if (catch_effect) resume_effect(catch_effect);
- else catch_effect = branch(() => catch_fn(anchor, error_source));
- }
-
- if (state !== PENDING && pending_effect) {
- pause_effect(pending_effect, () => (pending_effect = null));
- }
-
- if (state !== THEN && then_effect) {
- pause_effect(then_effect, () => (then_effect = null));
- }
-
- if (state !== CATCH && catch_effect) {
- pause_effect(catch_effect, () => (catch_effect = null));
- }
- } finally {
- if (restore) {
- if (DEV) {
- set_dev_current_component_function(null);
- set_dev_stack(null);
- }
- set_component_context(null);
- set_active_reaction(null);
- set_active_effect(null);
+ var v = /** @type {V} */ (UNINITIALIZED);
+ var value = runes ? source(v) : mutable_source(v, false, false);
+ var error = runes ? source(v) : mutable_source(v, false, false);
- // without this, the DOM does not update until two ticks after the promise
- // resolves, which is unexpected behaviour (and somewhat irksome to test)
- if (!is_flushing_sync) flushSync();
- }
- }
- }
+ var branches = new BranchManager(node);
- var effect = block(() => {
- if (input === (input = get_input())) return;
+ block(() => {
+ var input = get_input();
+ var destroyed = false;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
- // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
- let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
+ // @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
+ let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
- anchor = skip_nodes();
-
- set_hydrate_node(anchor);
+ set_hydrate_node(skip_nodes());
set_hydrating(false);
- mismatch = true;
}
if (is_promise(input)) {
- var promise = input;
+ var restore = capture();
+ var resolved = false;
+
+ /**
+ * @param {() => void} fn
+ */
+ const resolve = (fn) => {
+ if (destroyed) return;
+
+ resolved = true;
+ restore();
+
+ if (hydrating) {
+ // `restore()` could set `hydrating` to `true`, which we very much
+ // don't want — we want to restore everything _except_ this
+ set_hydrating(false);
+ }
- resolved = false;
+ try {
+ fn();
+ } finally {
+ unset_context();
- promise.then(
- (value) => {
- if (promise !== input) return;
- // we technically could use `set` here since it's on the next microtick
- // but let's use internal_set for consistency and just to be safe
- internal_set(input_source, value);
- update(THEN, true);
+ // without this, the DOM does not update until two ticks after the promise
+ // resolves, which is unexpected behaviour (and somewhat irksome to test)
+ if (!is_flushing_sync) flushSync();
+ }
+ };
+
+ input.then(
+ (v) => {
+ resolve(() => {
+ internal_set(value, v);
+ branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
+ });
},
- (error) => {
- if (promise !== input) return;
- // we technically could use `set` here since it's on the next microtick
- // but let's use internal_set for consistency and just to be safe
- internal_set(error_source, error);
- update(CATCH, true);
- if (!catch_fn) {
- // Rethrow the error if no catch block exists
- throw error_source.v;
- }
+ (e) => {
+ resolve(() => {
+ internal_set(error, e);
+ branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error)));
+
+ if (!catch_fn) {
+ // Rethrow the error if no catch block exists
+ throw error.v;
+ }
+ });
}
);
if (hydrating) {
- if (pending_fn) {
- pending_effect = branch(() => pending_fn(anchor));
- }
+ branches.ensure(PENDING, pending_fn);
} else {
// Wait a microtask before checking if we should show the pending state as
- // the promise might have resolved by the next microtask.
+ // the promise might have resolved by then
queue_micro_task(() => {
- if (!resolved) update(PENDING, true);
+ if (!resolved) {
+ resolve(() => {
+ branches.ensure(PENDING, pending_fn);
+ });
+ }
});
}
} else {
- internal_set(input_source, input);
- update(THEN, false);
+ internal_set(value, input);
+ branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
}
if (mismatch) {
@@ -194,11 +131,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_hydrating(true);
}
- // Set the input to something else, in order to disable the promise callbacks
- return () => (input = UNINITIALIZED);
+ return () => {
+ destroyed = true;
+ };
});
-
- if (hydrating) {
- anchor = hydrate_node;
- }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 4945cc16d0..3da9204571 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -8,7 +8,13 @@ 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, pause_effect } from '../../reactivity/effects.js';
+import {
+ block,
+ branch,
+ destroy_effect,
+ move_effect,
+ pause_effect
+} from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
@@ -418,24 +424,6 @@ 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_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js
new file mode 100644
index 0000000000..827f9f44fa
--- /dev/null
+++ b/packages/svelte/src/internal/client/dom/blocks/branches.js
@@ -0,0 +1,185 @@
+/** @import { Effect, TemplateNode } from '#client' */
+import { is_runes } from '../../context.js';
+import { Batch, current_batch } from '../../reactivity/batch.js';
+import {
+ branch,
+ destroy_effect,
+ move_effect,
+ pause_effect,
+ resume_effect
+} from '../../reactivity/effects.js';
+import { set_should_intro, should_intro } from '../../render.js';
+import { hydrate_node, hydrating } from '../hydration.js';
+import { create_text, should_defer_append } from '../operations.js';
+
+/**
+ * @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch
+ */
+
+/**
+ * @template Key
+ */
+export class BranchManager {
+ /** @type {TemplateNode} */
+ anchor;
+
+ /** @type {Map} */
+ #batches = new Map();
+
+ /** @type {Map} */
+ #onscreen = new Map();
+
+ /** @type {Map} */
+ #offscreen = new Map();
+
+ /**
+ * Whether to pause (i.e. outro) on change, or destroy immediately.
+ * This is necessary for ``
+ */
+ #transition = true;
+
+ /**
+ * @param {TemplateNode} anchor
+ * @param {boolean} transition
+ */
+ constructor(anchor, transition = true) {
+ this.anchor = anchor;
+ this.#transition = transition;
+ }
+
+ #commit = () => {
+ var batch = /** @type {Batch} */ (current_batch);
+
+ // if this batch was made obsolete, bail
+ if (!this.#batches.has(batch)) return;
+
+ var key = /** @type {Key} */ (this.#batches.get(batch));
+
+ var onscreen = this.#onscreen.get(key);
+
+ if (onscreen) {
+ // effect is already in the DOM — abort any current outro
+ resume_effect(onscreen);
+ } else {
+ // effect is currently offscreen. put it in the DOM
+ var offscreen = this.#offscreen.get(key);
+
+ if (offscreen) {
+ this.#onscreen.set(key, offscreen.effect);
+ this.#offscreen.delete(key);
+
+ // remove the anchor...
+ /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove();
+
+ // ...and append the fragment
+ this.anchor.before(offscreen.fragment);
+ onscreen = offscreen.effect;
+ }
+ }
+
+ for (const [b, k] of this.#batches) {
+ this.#batches.delete(b);
+
+ if (b === batch) {
+ // keep values for newer batches
+ break;
+ }
+
+ const offscreen = this.#offscreen.get(k);
+
+ if (offscreen) {
+ // for older batches, destroy offscreen effects
+ // as they will never be committed
+ destroy_effect(offscreen.effect);
+ this.#offscreen.delete(k);
+ }
+ }
+
+ // outro/destroy all onscreen effects...
+ for (const [k, effect] of this.#onscreen) {
+ // ...except the one that was just committed
+ if (k === key) continue;
+
+ const on_destroy = () => {
+ const keys = Array.from(this.#batches.values());
+
+ if (keys.includes(k)) {
+ // keep the effect offscreen, as another batch will need it
+ var fragment = document.createDocumentFragment();
+ move_effect(effect, fragment);
+
+ fragment.append(create_text()); // TODO can we avoid this?
+
+ this.#offscreen.set(k, { effect, fragment });
+ } else {
+ destroy_effect(effect);
+ }
+
+ this.#onscreen.delete(k);
+ };
+
+ if (this.#transition || !onscreen) {
+ pause_effect(effect, on_destroy, false);
+ } else {
+ on_destroy();
+ }
+ }
+ };
+
+ /**
+ *
+ * @param {any} key
+ * @param {null | ((target: TemplateNode) => void)} fn
+ */
+ ensure(key, fn) {
+ var batch = /** @type {Batch} */ (current_batch);
+ var defer = should_defer_append();
+
+ if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) {
+ if (defer) {
+ var fragment = document.createDocumentFragment();
+ var target = create_text();
+
+ fragment.append(target);
+
+ this.#offscreen.set(key, {
+ effect: branch(() => fn(target)),
+ fragment
+ });
+ } else {
+ this.#onscreen.set(
+ key,
+ branch(() => fn(this.anchor))
+ );
+ }
+ }
+
+ this.#batches.set(batch, key);
+
+ if (defer) {
+ for (const [k, effect] of this.#onscreen) {
+ if (k === key) {
+ batch.skipped_effects.delete(effect);
+ } else {
+ batch.skipped_effects.add(effect);
+ }
+ }
+
+ for (const [k, branch] of this.#offscreen) {
+ if (k === key) {
+ batch.skipped_effects.delete(branch.effect);
+ } else {
+ batch.skipped_effects.add(branch.effect);
+ }
+ }
+
+ batch.add_callback(this.#commit);
+ } else {
+ if (hydrating) {
+ this.anchor = hydrate_node;
+ }
+
+ this.#commit();
+ }
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 6349ab8399..7fa5ca464d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -1,19 +1,16 @@
-/** @import { Effect, TemplateNode } from '#client' */
-/** @import { Batch } from '../../reactivity/batch.js'; */
+/** @import { TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
- hydrate_node,
hydrating,
read_hydration_instruction,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
-import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.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';
+import { block } from '../../reactivity/effects.js';
+import { HYDRATION_START_ELSE } from '../../../../constants.js';
+import { BranchManager } from './branches.js';
// TODO reinstate https://github.com/sveltejs/svelte/pull/15250
@@ -28,122 +25,46 @@ export function if_block(node, fn, elseif = false) {
hydrate_next();
}
- var anchor = node;
-
- /** @type {Effect | null} */
- var consequent_effect = null;
-
- /** @type {Effect | null} */
- var alternate_effect = null;
-
- /** @type {typeof UNINITIALIZED | boolean | null} */
- var condition = UNINITIALIZED;
-
+ var branches = new BranchManager(node);
var flags = elseif ? EFFECT_TRANSPARENT : 0;
- var has_branch = false;
-
- 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) => 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;
-
+ /**
+ * @param {boolean} condition,
+ * @param {null | ((anchor: Node) => void)} fn
+ */
+ function update_branch(condition, fn) {
if (hydrating) {
- const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
+ const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE;
- if (!!condition === is_else) {
+ if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
- anchor = skip_nodes();
+ var anchor = skip_nodes();
set_hydrate_node(anchor);
- set_hydrating(false);
- mismatch = true;
- }
- }
+ branches.anchor = anchor;
- var defer = should_defer_append();
- var target = anchor;
-
- if (defer) {
- offscreen_fragment = document.createDocumentFragment();
- offscreen_fragment.append((target = create_text()));
- }
+ set_hydrating(false);
+ branches.ensure(condition, fn);
+ set_hydrating(true);
- if (condition) {
- consequent_effect ??= fn && branch(() => fn(target));
- } else {
- alternate_effect ??= fn && branch(() => fn(target));
+ return;
+ }
}
- 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);
+ branches.ensure(condition, fn);
+ }
- batch.add_callback(commit);
- } else {
- commit();
- }
+ block(() => {
+ var has_branch = false;
- if (mismatch) {
- // continue in hydration mode
- set_hydrating(true);
- }
- };
+ fn((fn, flag = true) => {
+ has_branch = true;
+ update_branch(flag, fn);
+ });
- block(() => {
- has_branch = false;
- fn(set_branch);
if (!has_branch) {
- update_branch(null, null);
+ update_branch(false, null);
}
}, flags);
-
- if (hydrating) {
- anchor = hydrate_node;
- }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js
index 5e3c42019f..849b1c2447 100644
--- a/packages/svelte/src/internal/client/dom/blocks/key.js
+++ b/packages/svelte/src/internal/client/dom/blocks/key.js
@@ -1,12 +1,8 @@
-/** @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 { TemplateNode } from '#client' */
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';
+import { block } from '../../reactivity/effects.js';
+import { hydrate_next, hydrating } from '../hydration.js';
+import { BranchManager } from './branches.js';
/**
* @template V
@@ -20,60 +16,18 @@ export function key(node, get_key, render_fn) {
hydrate_next();
}
- var anchor = node;
+ var branches = new BranchManager(node);
- /** @type {V | typeof UNINITIALIZED} */
- var key = UNINITIALIZED;
-
- /** @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;
- }
+ var legacy = !is_runes();
block(() => {
- if (changed(key, (key = get_key()))) {
- var target = anchor;
-
- var defer = should_defer_append();
-
- if (defer) {
- offscreen_fragment = document.createDocumentFragment();
- offscreen_fragment.append((target = create_text()));
- }
-
- pending_effect = branch(() => render_fn(target));
+ var key = get_key();
- if (defer) {
- /** @type {Batch} */ (current_batch).add_callback(commit);
- } else {
- commit();
- }
+ // key blocks in Svelte <5 had stupid semantics
+ if (legacy && key !== null && typeof key === 'object') {
+ key = /** @type {V} */ ({});
}
- });
- if (hydrating) {
- anchor = hydrate_node;
- }
+ branches.ensure(key, render_fn);
+ });
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js
index 32d88d4c60..0c4948aca0 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -1,8 +1,8 @@
/** @import { Snippet } from 'svelte' */
-/** @import { Effect, TemplateNode } from '#client' */
+/** @import { TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
-import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
+import { block, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
set_dev_current_component_function
@@ -14,8 +14,8 @@ import * as w from '../../warnings.js';
import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
-import { noop } from '../../../shared/utils.js';
import { prevent_snippet_stringification } from '../../../shared/validate.js';
+import { BranchManager } from './branches.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -25,33 +25,17 @@ import { prevent_snippet_stringification } from '../../../shared/validate.js';
* @returns {void}
*/
export function snippet(node, get_snippet, ...args) {
- var anchor = node;
-
- /** @type {SnippetFn | null | undefined} */
- // @ts-ignore
- var snippet = noop;
-
- /** @type {Effect | null} */
- var snippet_effect;
+ var branches = new BranchManager(node);
block(() => {
- if (snippet === (snippet = get_snippet())) return;
-
- if (snippet_effect) {
- destroy_effect(snippet_effect);
- snippet_effect = null;
- }
+ const snippet = get_snippet() ?? null;
if (DEV && snippet == null) {
e.invalid_snippet();
}
- snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
+ branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args)));
}, EFFECT_TRANSPARENT);
-
- if (hydrating) {
- anchor = hydrate_node;
- }
}
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index 2697722b39..134e57e627 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -1,10 +1,8 @@
-/** @import { TemplateNode, Dom, Effect } from '#client' */
-/** @import { Batch } from '../../reactivity/batch.js'; */
+/** @import { TemplateNode, Dom } from '#client' */
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';
+import { block } from '../../reactivity/effects.js';
+import { hydrate_next, hydrating } from '../hydration.js';
+import { BranchManager } from './branches.js';
/**
* @template P
@@ -19,64 +17,10 @@ export function component(node, get_component, render_fn) {
hydrate_next();
}
- var anchor = node;
-
- /** @type {C} */
- var component;
-
- /** @type {Effect | null} */
- var effect;
-
- /** @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;
- }
+ var branches = new BranchManager(node);
block(() => {
- if (component === (component = get_component())) return;
-
- var defer = should_defer_append();
-
- if (component) {
- var target = anchor;
-
- if (defer) {
- offscreen_fragment = document.createDocumentFragment();
- offscreen_fragment.append((target = create_text()));
- if (effect) {
- /** @type {Batch} */ (current_batch).skipped_effects.add(effect);
- }
- }
- pending_effect = branch(() => render_fn(target, component));
- }
-
- if (defer) {
- /** @type {Batch} */ (current_batch).add_callback(commit);
- } else {
- commit();
- }
+ var component = get_component() ?? null;
+ branches.ensure(component, component && ((target) => render_fn(target, component)));
}, EFFECT_TRANSPARENT);
-
- if (hydrating) {
- anchor = hydrate_node;
- }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 231a3621b1..6533ff8921 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -8,13 +8,7 @@ import {
set_hydrating
} from '../hydration.js';
import { create_text, get_first_child } from '../operations.js';
-import {
- block,
- branch,
- destroy_effect,
- pause_effect,
- resume_effect
-} from '../../reactivity/effects.js';
+import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
@@ -23,6 +17,7 @@ import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
+import { BranchManager } from './branches.js';
/**
* @param {Comment | Element} node
@@ -42,12 +37,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
var filename = DEV && location && component_context?.function[FILENAME];
- /** @type {string | null} */
- var tag;
-
- /** @type {string | null} */
- var current_tag;
-
/** @type {null | Element} */
var element = null;
@@ -58,9 +47,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node);
- /** @type {Effect | null} */
- var effect;
-
/**
* The keyed `{#each ...}` item block, if any, that this element is inside.
* We track this so we can set it when changing the element, allowing any
@@ -68,36 +54,24 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
*/
var each_item_block = current_each_item;
+ var branches = new BranchManager(anchor, false);
+
block(() => {
const next_tag = get_tag() || null;
var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null;
- // Assumption: Noone changes the namespace but not the tag (what would that even mean?)
- if (next_tag === tag) return;
-
- // See explanation of `each_item_block` above
- var previous_each_item = current_each_item;
- set_current_each_item(each_item_block);
-
- if (effect) {
- if (next_tag === null) {
- // start outro
- pause_effect(effect, () => {
- effect = null;
- current_tag = null;
- });
- } else if (next_tag === current_tag) {
- // same tag as is currently rendered — abort outro
- resume_effect(effect);
- } else {
- // tag is changing — destroy immediately, render contents without intro transitions
- destroy_effect(effect);
- set_should_intro(false);
- }
+ if (next_tag === null) {
+ branches.ensure(null, null);
+ set_should_intro(true);
+ return;
}
- if (next_tag && next_tag !== current_tag) {
- effect = branch(() => {
+ branches.ensure(next_tag, (anchor) => {
+ // See explanation of `each_item_block` above
+ var previous_each_item = current_each_item;
+ set_current_each_item(each_item_block);
+
+ if (next_tag) {
element = hydrating
? /** @type {Element} */ (element)
: ns
@@ -149,16 +123,31 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
/** @type {Effect} */ (active_effect).nodes_end = element;
anchor.before(element);
- });
- }
+ }
+
+ set_current_each_item(previous_each_item);
+
+ if (hydrating) {
+ set_hydrate_node(anchor);
+ }
+ });
- tag = next_tag;
- if (tag) current_tag = tag;
+ // revert to the default state after the effect has been created
set_should_intro(true);
- set_current_each_item(previous_each_item);
+ return () => {
+ if (next_tag) {
+ // if we're in this callback because we're re-running the effect,
+ // disable intros (unless no element is currently displayed)
+ set_should_intro(false);
+ }
+ };
}, EFFECT_TRANSPARENT);
+ teardown(() => {
+ set_should_intro(true);
+ });
+
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(anchor);
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
index e39fb865cd..46e8f524f8 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
@@ -3,6 +3,7 @@ import { listen_to_event_and_reset_event } from './shared.js';
import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.js';
+import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
@@ -83,6 +84,7 @@ export function init_select(select) {
* @returns {void}
*/
export function bind_select_value(select, get, set = get) {
+ var batches = new WeakSet();
var mounting = true;
listen_to_event_and_reset_event(select, 'change', (is_reset) => {
@@ -102,11 +104,30 @@ export function bind_select_value(select, get, set = get) {
}
set(value);
+
+ if (current_batch !== null) {
+ batches.add(current_batch);
+ }
});
// Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated
effect(() => {
var value = get();
+
+ if (select === document.activeElement) {
+ // we need both, because in non-async mode, render effects run before previous_batch is set
+ var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
+
+ // Don't update the if it is focused. We can get here if, for example,
+ // an update is deferred because of async work depending on the select:
+ //
+ // ...
+ // {await find(selected)}
+ if (batches.has(batch)) {
+ return;
+ }
+ }
+
select_option(select, value, mounting);
// Mounting and value undefined -> take selection from dom
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 3c5409bcfe..471eed299d 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -103,7 +103,7 @@ export {
save,
track_reactivity_loss
} from './reactivity/async.js';
-export { flushSync as flush } from './reactivity/batch.js';
+export { eager, flushSync as flush } from './reactivity/batch.js';
export {
async_derived,
user_derived as derived,
diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js
index a223d1b5be..24ff4793ea 100644
--- a/packages/svelte/src/internal/client/reactivity/async.js
+++ b/packages/svelte/src/internal/client/reactivity/async.js
@@ -1,8 +1,13 @@
-/** @import { Effect, Value } from '#client' */
-
+/** @import { Effect, TemplateNode, 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 {
+ component_context,
+ dev_stack,
+ is_runes,
+ set_component_context,
+ set_dev_stack
+} from '../context.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
@@ -28,6 +33,7 @@ import {
set_hydrating,
skip_nodes
} from '../dom/hydration.js';
+import { create_text } from '../dom/operations.js';
/**
*
@@ -80,7 +86,7 @@ export function flatten(sync, async, fn) {
* some asynchronous work has happened (so that e.g. `await a + b`
* causes `b` to be registered as a dependency).
*/
-function capture() {
+export function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
@@ -92,6 +98,10 @@ function capture() {
var previous_hydrate_node = hydrate_node;
}
+ if (DEV) {
+ var previous_dev_stack = dev_stack;
+ }
+
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
@@ -105,6 +115,7 @@ function capture() {
if (DEV) {
set_from_async_derived(null);
+ set_dev_stack(previous_dev_stack);
}
};
}
@@ -193,13 +204,18 @@ export function unset_context() {
set_active_effect(null);
set_active_reaction(null);
set_component_context(null);
- if (DEV) set_from_async_derived(null);
+
+ if (DEV) {
+ set_from_async_derived(null);
+ set_dev_stack(null);
+ }
}
/**
- * @param {() => Promise} fn
+ * @param {TemplateNode} anchor
+ * @param {(target: TemplateNode) => Promise} fn
*/
-export async function async_body(fn) {
+export async function async_body(anchor, fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
@@ -219,7 +235,7 @@ export async function async_body(fn) {
}
try {
- var promise = fn();
+ var promise = fn(anchor);
} finally {
if (next_hydrate_node) {
set_hydrate_node(next_hydrate_node);
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index b74ce0ba9b..a9973b465b 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -18,6 +18,7 @@ import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
+ get,
is_dirty,
is_updating_effect,
set_is_updating_effect,
@@ -28,8 +29,8 @@ import * as e from '../errors.js';
import { flush_tasks, queue_micro_task } 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 { old_values, source, update } from './sources.js';
+import { inspect_effect, unlink_effect } from './effects.js';
/**
* @typedef {{
@@ -726,6 +727,65 @@ export function schedule_effect(signal) {
queued_root_effects.push(effect);
}
+/** @type {Source[]} */
+let eager_versions = [];
+
+function eager_flush() {
+ try {
+ flushSync(() => {
+ for (const version of eager_versions) {
+ update(version);
+ }
+ });
+ } finally {
+ eager_versions = [];
+ }
+}
+
+/**
+ * Implementation of `$state.eager(fn())`
+ * @template T
+ * @param {() => T} fn
+ * @returns {T}
+ */
+export function eager(fn) {
+ var version = source(0);
+ var initial = true;
+ var value = /** @type {T} */ (undefined);
+
+ get(version);
+
+ inspect_effect(() => {
+ if (initial) {
+ // the first time this runs, we create an inspect effect
+ // that will run eagerly whenever the expression changes
+ var previous_batch_values = batch_values;
+
+ try {
+ batch_values = null;
+ value = fn();
+ } finally {
+ batch_values = previous_batch_values;
+ }
+
+ return;
+ }
+
+ // the second time this effect runs, it's to schedule a
+ // `version` update. since this will recreate the effect,
+ // we don't need to evaluate the expression here
+ if (eager_versions.length === 0) {
+ queue_micro_task(eager_flush);
+ }
+
+ eager_versions.push(version);
+ });
+
+ initial = false;
+
+ return value;
+}
+
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 2c9e4db911..bfbb95a8db 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -553,15 +553,16 @@ export function unlink_effect(effect) {
* A paused effect does not update, and the DOM subtree becomes inert.
* @param {Effect} effect
* @param {() => void} [callback]
+ * @param {boolean} [destroy]
*/
-export function pause_effect(effect, callback) {
+export function pause_effect(effect, callback, destroy = true) {
/** @type {TransitionManager[]} */
var transitions = [];
pause_children(effect, transitions, true);
run_out_transitions(transitions, () => {
- destroy_effect(effect);
+ if (destroy) destroy_effect(effect);
if (callback) callback();
});
}
@@ -662,3 +663,20 @@ function resume_children(effect, local) {
export function aborted(effect = /** @type {Effect} */ (active_effect)) {
return (effect.f & DESTROYED) !== 0;
}
+
+/**
+ * @param {Effect} effect
+ * @param {DocumentFragment} fragment
+ */
+export 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;
+ }
+}
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index f8a7e8d46d..a54a421418 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -436,6 +436,7 @@ const STATE_CREATION_RUNES = /** @type {const} */ ([
const RUNES = /** @type {const} */ ([
...STATE_CREATION_RUNES,
+ '$state.eager',
'$state.snapshot',
'$props',
'$props.id',
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 021668f1e6..e33d22d4c4 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.40.2';
+export const VERSION = '5.41.1';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js
index 7424278180..be9d5a483f 100644
--- a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js
@@ -4,7 +4,7 @@ export default test({
async: true,
error: {
code: 'const_tag_invalid_reference',
- message: 'The `{@const foo = ...}` declaration is not available in this snippet ',
+ message: 'The `{@const foo = ...}` declaration is not available in this snippet',
position: [376, 379]
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js
index 7ff71a61f9..5132bd93b7 100644
--- a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js
@@ -4,7 +4,7 @@ export default test({
async: true,
error: {
code: 'const_tag_invalid_reference',
- message: 'The `{@const foo = ...}` declaration is not available in this snippet ',
+ message: 'The `{@const foo = ...}` declaration is not available in this snippet',
position: [298, 301]
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js
new file mode 100644
index 0000000000..923fe0c0ac
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'each_key_without_as',
+ message: 'An `{#each ...}` block without an `as` clause cannot have a key'
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte
new file mode 100644
index 0000000000..794740de8f
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte
@@ -0,0 +1,7 @@
+
+
+{#each items, i (items[i].id)}
+ {items[i].id}
+{/each}
diff --git a/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js
new file mode 100644
index 0000000000..2f7a7863a7
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: 'foo'
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte
new file mode 100644
index 0000000000..44e700bdd4
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte
new file mode 100644
index 0000000000..abca25bab2
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte
@@ -0,0 +1,7 @@
+
+
+ {@const thing = data}
+ {thing}
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js
index b0772ad3c0..76a2032c7a 100644
--- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js
@@ -2,8 +2,9 @@ import { tick } from 'svelte';
import { test } from '../../test';
export default test({
- async test({ assert, target, instance }) {
- instance.shift();
+ async test({ assert, target }) {
+ const [shift] = target.querySelectorAll('button');
+ shift.click();
await tick();
const [input] = target.querySelectorAll('input');
@@ -13,7 +14,7 @@ export default test({
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
- assert.htmlEqual(target.innerHTML, ` 0
`);
+ assert.htmlEqual(target.innerHTML, `shift 0
`);
assert.equal(input.value, '1');
input.focus();
@@ -21,17 +22,17 @@ export default test({
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
- assert.htmlEqual(target.innerHTML, ` 0
`);
+ assert.htmlEqual(target.innerHTML, `shift 0
`);
assert.equal(input.value, '2');
- instance.shift();
+ shift.click();
await tick();
- assert.htmlEqual(target.innerHTML, ` 1
`);
+ assert.htmlEqual(target.innerHTML, `shift 1
`);
assert.equal(input.value, '2');
- instance.shift();
+ shift.click();
await tick();
- assert.htmlEqual(target.innerHTML, ` 2
`);
+ assert.htmlEqual(target.innerHTML, `shift 2
`);
assert.equal(input.value, '2');
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte
index 2fc898e654..e2f01a66c8 100644
--- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte
@@ -1,22 +1,23 @@
+ {
+ input.focus();
+ resolvers.shift()?.();
+}}>shift
+
-
+
{await push(count)}
{#snippet pending()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js
new file mode 100644
index 0000000000..7fddca0d58
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js
@@ -0,0 +1,82 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [shift] = target.querySelectorAll('button');
+ shift.click();
+ await tick();
+
+ const [select] = target.querySelectorAll('select');
+
+ select.focus();
+ select.value = 'three';
+ select.dispatchEvent(new InputEvent('change', { bubbles: true }));
+ await tick();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ shift
+
+ one
+ two
+ three
+
+ two
+ `
+ );
+ assert.equal(select.value, 'three');
+
+ select.focus();
+ select.value = 'one';
+ select.dispatchEvent(new InputEvent('change', { bubbles: true }));
+ await tick();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ shift
+
+ one
+ two
+ three
+
+ two
+ `
+ );
+ assert.equal(select.value, 'one');
+
+ shift.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ shift
+
+ one
+ two
+ three
+
+ three
+ `
+ );
+ assert.equal(select.value, 'one');
+
+ shift.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ shift
+
+ one
+ two
+ three
+
+ one
+ `
+ );
+ assert.equal(select.value, 'one');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte
new file mode 100644
index 0000000000..566ea60ec5
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte
@@ -0,0 +1,31 @@
+
+
+ {
+ select.focus();
+ resolvers.shift()?.();
+}}>shift
+
+
+
+ one
+ two
+ three
+
+
+ {await push(selected)}
+
+ {#snippet pending()}
+ loading...
+ {/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js
new file mode 100644
index 0000000000..f84228ec14
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js
@@ -0,0 +1,36 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [count, shift] = target.querySelectorAll('button');
+
+ shift.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `0 shift 0
`);
+
+ count.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `1 shift 0
`);
+
+ count.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `2 shift 0
`);
+
+ count.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `3 shift 0
`);
+
+ shift.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `3 shift 1
`);
+
+ shift.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `3 shift 2
`);
+
+ shift.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, `3 shift 3
`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-eager/main.svelte
new file mode 100644
index 0000000000..c9168b3984
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager/main.svelte
@@ -0,0 +1,20 @@
+
+
+ count += 1}>{$state.eager(count)}
+ resolvers.shift()?.()}>shift
+
+
+ {await push(count)}
+
+ {#snippet pending()}{/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js
index 1725cd8f6f..9ef598de6c 100644
--- a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js
@@ -1,3 +1,4 @@
+import { tick } from 'svelte';
import { test } from '../../test';
/**
@@ -77,7 +78,7 @@ export default test({
const { promise, reject } = promiseWithResolver();
component.promise = promise;
// wait for rendering
- await Promise.resolve();
+ await tick();
// remove the promise
component.promise = null;
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index f01edd947f..d260b738c3 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -3193,6 +3193,18 @@ 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
+ *
+ * home
+ * about
+ *
+ * ```
+ */
+ export function eager(value: T): T;
/**
* Declares state that is _not_ made deeply reactive — instead of mutating it,
* you must reassign it.