Merge branch 'main' into gh-17012

gh-17012
ComputerGuy 4 weeks ago committed by GitHub
commit 07040d1005
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take into account static blocks when determining transition locality

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: flush pending changes after rendering `failed` snippet

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: improve `each_key_without_as` error

@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)):
@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
```
A convenient way to find the origin of some change is to pass `console.trace` to `with`:
```js
// @errors: 2304
$inspect(stuff).with(console.trace);
```
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.

@ -1,5 +1,35 @@
# svelte
## 5.41.3
### Patch Changes
- chore: exclude vite optimized deps from stack traces ([#17008](https://github.com/sveltejs/svelte/pull/17008))
- perf: skip repeatedly traversing the same derived ([#17016](https://github.com/sveltejs/svelte/pull/17016))
## 5.41.2
### Patch Changes
- fix: keep batches alive until all async work is complete ([#16971](https://github.com/sveltejs/svelte/pull/16971))
- fix: don't preserve reactivity context across function boundaries ([#17002](https://github.com/sveltejs/svelte/pull/17002))
- fix: make `$inspect` logs come from the callsite ([#17001](https://github.com/sveltejs/svelte/pull/17001))
- fix: ensure guards (eg. if, each, key) run before their contents ([#16930](https://github.com/sveltejs/svelte/pull/16930))
## 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

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

@ -306,7 +306,7 @@ export function analyze_module(source, options) {
fragment: null,
parent_element: null,
reactive_statement: null,
in_derived: false
derived_function_depth: -1
},
visitors
);
@ -703,7 +703,7 @@ export function analyze_component(root, source, options) {
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null,
in_derived: false
derived_function_depth: -1
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -771,7 +771,7 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
in_derived: false
derived_function_depth: -1
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

@ -29,9 +29,9 @@ export interface AnalysisState {
reactive_statement: null | ReactiveStatement;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
* Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
*/
in_derived: boolean;
derived_function_depth: number;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -15,7 +15,10 @@ export function AwaitExpression(node, context) {
// b) awaits that precede other expressions in template or `$derived(...)`
if (
tla ||
(is_reactive_expression(context.path, context.state.in_derived) &&
(is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node))
) {
context.state.analysis.pickled_awaits.add(node);
@ -53,9 +56,7 @@ export function AwaitExpression(node, context) {
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) {
return true;
}
if (in_derived) return true;
let i = path.length;
@ -67,6 +68,7 @@ export function is_reactive_expression(path, in_derived) {
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
// No reactive expression found between function and await
return false;
}
@ -83,11 +85,16 @@ export function is_reactive_expression(path, in_derived) {
* @param {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
export function is_last_evaluated_expression(path, node) {
function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]);
const parent = path[i];
if (parent.type === 'ConstTag') {
// {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {

@ -248,7 +248,7 @@ export function CallExpression(node, context) {
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
in_derived: true,
derived_function_depth: context.state.function_depth + 1,
expression
});

@ -38,6 +38,8 @@ export function ConstTag(node, context) {
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
in_derived: true
// We're treating this like a $derived under the hood
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1
});
}

@ -64,12 +64,6 @@ export function VariableDeclarator(node, context) {
}
}
if (rune === '$derived') {
context.visit(node.id);
context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true });
return;
}
if (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);

@ -177,6 +177,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),
@ -389,7 +390,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,

@ -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 */

@ -1,10 +1,10 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
import { get_inspect_args } from '../../utils.js';
/**
* @param {CallExpression} node
@ -73,7 +73,7 @@ export function CallExpression(node, context) {
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
return transform_inspect_rune(rune, node, context);
}
if (
@ -104,3 +104,21 @@ export function CallExpression(node, context) {
context.next();
}
/**
* @param {'$inspect' | '$inspect().with'} rune
* @param {CallExpression} node
* @param {Context} context
*/
function transform_inspect_rune(rune, node, context) {
if (!dev) return b.empty;
const { args, inspector } = get_inspect_args(rune, node, context.visit);
// by passing an arrow function, the log appears to come from the `$inspect` callsite
// rather than the `inspect.js` file containing the utility
const id = b.id('$$args');
const fn = b.arrow([b.rest(id)], b.call(inspector, b.spread(id)));
return b.call('$.inspect', b.thunk(b.array(args)), fn, rune === '$inspect' && b.true);
}

@ -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);
}

@ -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)))
);
}
}

@ -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':

@ -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 });
}
}

@ -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);
}
}

@ -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) {

@ -1,9 +1,9 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.js';
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { get_inspect_args } from '../../utils.js';
/**
* @param {CallExpression} node
@ -51,7 +51,13 @@ export function CallExpression(node, context) {
}
if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
if (!dev) return b.empty;
const { args, inspector } = get_inspect_args(rune, node, context.visit);
return rune === '$inspect'
? b.call(inspector, b.literal('$inspect('), ...args, b.literal(')'))
: b.call(inspector, b.literal('init'), ...args);
}
context.next();

@ -1,7 +1,7 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */
import {
regex_ends_with_whitespaces,
regex_not_whitespace,
@ -452,30 +452,19 @@ export function determine_namespace_for_children(node, namespace) {
}
/**
* @template {TransformState} T
* @param {'$inspect' | '$inspect().with'} rune
* @param {CallExpression} node
* @param {Context<any, T>} context
* @param {(node: AST.SvelteNode) => AST.SvelteNode} visit
*/
export function transform_inspect_rune(node, context) {
const { state, visit } = context;
const as_fn = state.options.generate === 'client';
if (!dev) return b.empty;
if (node.callee.type === 'MemberExpression') {
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;
const inspect_args =
/** @type {Array<Expression>} */
(raw_inspect_args.map((arg) => visit(arg)));
const with_arg = /** @type {Expression} */ (visit(node.arguments[0]));
return b.call(
'$.inspect',
as_fn ? b.thunk(b.array(inspect_args)) : b.array(inspect_args),
with_arg
);
} else {
const arg = node.arguments.map((arg) => /** @type {Expression} */ (visit(arg)));
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
}
export function get_inspect_args(rune, node, visit) {
const call =
rune === '$inspect'
? node
: /** @type {CallExpression} */ (/** @type {MemberExpression} */ (node.callee).object);
return {
args: call.arguments.map((arg) => /** @type {Expression} */ (visit(arg))),
inspector:
rune === '$inspect' ? 'console.log' : /** @type {Expression} */ (visit(node.arguments[0]))
};
}

@ -1,3 +1,4 @@
// General flags
export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const RENDER_EFFECT = 1 << 3;
@ -5,21 +6,34 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
// Flags exclusive to effects
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
/**
* 'Transparent' effects do not create a transition boundary.
* This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
*/
export const EFFECT_TRANSPARENT = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
// Flags exclusive to deriveds
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
*/
export const WAS_MARKED = 1 << 15;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;

@ -2,13 +2,14 @@ import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
import { get_stack } from './tracing.js';
/**
* @param {() => any[]} get_value
* @param {Function} [inspector]
* @param {Function} inspector
* @param {boolean} show_stack
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspector = console.log) {
export function inspect(get_value, inspector, show_stack = false) {
validate_effect('$inspect');
let initial = true;
@ -28,7 +29,16 @@ export function inspect(get_value, inspector = console.log) {
var snap = snapshot(value, true, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
if (show_stack) {
inspector(...snap);
if (!initial) {
// eslint-disable-next-line no-console
console.log(get_stack('UpdatedAt'));
}
} else {
inspector(initial ? 'init' : 'update', ...snap);
}
});
initial = false;

@ -134,7 +134,16 @@ export function trace(label, fn) {
* @returns {Error & { stack: string } | null}
*/
export function get_stack(label) {
// @ts-ignore stackTraceLimit doesn't exist everywhere
const limit = Error.stackTraceLimit;
// @ts-ignore
Error.stackTraceLimit = Infinity;
let error = Error();
// @ts-ignore
Error.stackTraceLimit = limit;
const stack = error.stack;
if (!stack) return null;
@ -144,16 +153,20 @@ export function get_stack(label) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const posixified = line.replaceAll('\\', '/');
if (line === 'Error') {
continue;
}
if (line.includes('validate_each_keys')) {
return null;
}
if (line.includes('svelte/src/internal')) {
if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
continue;
}
new_lines.push(line);
}

@ -1,5 +1,6 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@ -18,8 +19,11 @@ import { get_boundary } from './boundary.js';
*/
export function async(node, expressions, fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
batch.increment(blocking);
var was_hydrating = hydrating;
@ -44,6 +48,7 @@ export function async(node, expressions, fn) {
fn(node, ...values);
} finally {
boundary.update_pending_count(-1);
batch.decrement(blocking);
}
if (was_hydrating) {

@ -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<V>)} get_input
* @param {(() => any)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => 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<V> | 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;
}
}

@ -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,
@ -24,7 +30,6 @@ import {
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
@ -285,13 +290,6 @@ export class Boundary {
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
// TODO this feels like a little bit of a kludge, but until we
// overhaul the boundary/batch relationship it's probably
// the most pragmatic solution available to us
queue_micro_task(() => {
Batch.ensure().flush();
});
}
}
@ -403,6 +401,7 @@ export class Boundary {
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
try {
@ -425,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);
}

@ -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<Batch, Key>} */
#batches = new Map();
/** @type {Map<Key, Effect>} */
#onscreen = new Map();
/** @type {Map<Key, Branch>} */
#offscreen = new Map();
/**
* Whether to pause (i.e. outro) on change, or destroy immediately.
* This is necessary for `<svelte:element>`
*/
#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();
}
}
}

@ -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;
}
}

@ -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);
});
}

@ -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;
}
}
/**

@ -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;
}
}

@ -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);

@ -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,19 +204,24 @@ 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<void>} fn
* @param {TemplateNode} anchor
* @param {(target: TemplateNode) => Promise<void>} fn
*/
export async function async_body(fn) {
export async function async_body(anchor, fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.is_pending();
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
if (!pending) batch.increment();
batch.increment(blocking);
var active = /** @type {Effect} */ (active_effect);
@ -218,7 +234,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);
@ -238,12 +254,7 @@ export async function async_body(fn) {
}
boundary.update_pending_count(-1);
if (pending) {
batch.flush();
} else {
batch.decrement();
}
batch.decrement(blocking);
unset_context();
}

@ -11,7 +11,8 @@ import {
RENDER_EFFECT,
ROOT_EFFECT,
MAYBE_DIRTY,
DERIVED
DERIVED,
BOUNDARY_EFFECT
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
@ -31,6 +32,16 @@ import { invoke_error_boundary } from '../error-handling.js';
import { old_values, source, update } from './sources.js';
import { inspect_effect, unlink_effect } from './effects.js';
/**
* @typedef {{
* parent: EffectTarget | null;
* effect: Effect | null;
* effects: Effect[];
* render_effects: Effect[];
* block_effects: Effect[];
* }} EffectTarget
*/
/** @type {Set<Batch>} */
const batches = new Set();
@ -65,6 +76,8 @@ let is_flushing = false;
export let is_flushing_sync = false;
export class Batch {
committed = false;
/**
* The current values of any sources that are updated in this batch
* They keys of this map are identical to `this.#previous`
@ -91,6 +104,11 @@ export class Batch {
*/
#pending = 0;
/**
* The number of async effects that are currently in flight, _not_ inside a pending boundary
*/
#blocking_pending = 0;
/**
* A deferred that resolves when the batch is committed, used with `settled()`
* TODO replace with Promise.withResolvers once supported widely enough
@ -98,26 +116,6 @@ export class Batch {
*/
#deferred = null;
/**
* Template effects and `$effect.pre` effects, which run when
* a batch is committed
* @type {Effect[]}
*/
#render_effects = [];
/**
* The same as `#render_effects`, but for `$effect` (which runs after)
* @type {Effect[]}
*/
#effects = [];
/**
* Block effects, which may need to re-run on subsequent flushes
* in order to update internal sources (e.g. each block items)
* @type {Effect[]}
*/
#block_effects = [];
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Effect[]}
@ -148,41 +146,37 @@ export class Batch {
this.apply();
/** @type {EffectTarget} */
var target = {
parent: null,
effect: null,
effects: [],
render_effects: [],
block_effects: []
};
for (const root of root_effects) {
this.#traverse_effect_tree(root);
this.#traverse_effect_tree(root, target);
}
// if there is no outstanding async work, 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();
this.#resolve();
var render_effects = this.#render_effects;
var effects = this.#effects;
this.#render_effects = [];
this.#effects = [];
this.#block_effects = [];
if (this.#blocking_pending > 0) {
this.#defer_effects(target.effects);
this.#defer_effects(target.render_effects);
this.#defer_effects(target.block_effects);
} else {
// TODO append/detach blocks here, not in #commit
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = this;
current_batch = null;
batch_values = previous_batch_sources;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
flush_queued_effects(target.render_effects);
flush_queued_effects(target.effects);
previous_batch = null;
this.#deferred?.resolve();
} else {
this.#defer_effects(this.#render_effects);
this.#defer_effects(this.#effects);
this.#defer_effects(this.#block_effects);
}
batch_values = null;
@ -192,8 +186,9 @@ export class Batch {
* Traverse the effect tree, executing effects or stashing
* them for later execution as appropriate
* @param {Effect} root
* @param {EffectTarget} target
*/
#traverse_effect_tree(root) {
#traverse_effect_tree(root, target) {
root.f ^= CLEAN;
var effect = root.first;
@ -205,15 +200,25 @@ export class Batch {
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
if ((effect.f & BOUNDARY_EFFECT) !== 0 && effect.b?.is_pending()) {
target = {
parent: target,
effect,
effects: [],
render_effects: [],
block_effects: []
};
}
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
target.effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
target.render_effects.push(effect);
} else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect);
update_effect(effect);
}
@ -229,6 +234,17 @@ export class Batch {
effect = effect.next;
while (effect === null && parent !== null) {
if (parent === target.effect) {
// TODO rather than traversing into pending boundaries and deferring the effects,
// could we just attach the effects _to_ the pending boundary and schedule them
// once the boundary is ready?
this.#defer_effects(target.effects);
this.#defer_effects(target.render_effects);
this.#defer_effects(target.block_effects);
target = /** @type {EffectTarget} */ (target.parent);
}
effect = parent.next;
parent = parent.parent;
}
@ -246,8 +262,6 @@ export class Batch {
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(e, CLEAN);
}
effects.length = 0;
}
/**
@ -283,8 +297,8 @@ export class Batch {
// this can happen if a new batch was created during `flush_effects()`
return;
}
} else if (this.#pending === 0) {
this.#commit();
} else {
this.#resolve();
}
this.deactivate();
@ -300,16 +314,19 @@ export class Batch {
}
}
/**
* Append and remove branches to/from the DOM
*/
#commit() {
for (const fn of this.#callbacks) {
fn();
#resolve() {
if (this.#blocking_pending === 0) {
// append/remove branches
for (const fn of this.#callbacks) fn();
this.#callbacks.clear();
}
this.#callbacks.clear();
if (this.#pending === 0) {
this.#commit();
}
}
#commit() {
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
@ -317,7 +334,17 @@ export class Batch {
if (batches.size > 1) {
this.#previous.clear();
let is_earlier = true;
var previous_batch_values = batch_values;
var is_earlier = true;
/** @type {EffectTarget} */
var dummy_target = {
parent: null,
effect: null,
effects: [],
render_effects: [],
block_effects: []
};
for (const batch of batches) {
if (batch === this) {
@ -350,8 +377,12 @@ export class Batch {
// 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) {
/** @type {Set<Value>} */
const marked = new Set();
/** @type {Map<Reaction, boolean>} */
const checked = new Map();
for (const source of sources) {
mark_effects(source, others);
mark_effects(source, others, marked, checked);
}
if (queued_root_effects.length > 0) {
@ -359,9 +390,11 @@ export class Batch {
batch.apply();
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root);
batch.#traverse_effect_tree(root, dummy_target);
}
// TODO do we need to do anything with `target`? defer block effects?
queued_root_effects = [];
batch.deactivate();
}
@ -369,17 +402,31 @@ export class Batch {
}
current_batch = null;
batch_values = previous_batch_values;
}
this.committed = true;
batches.delete(this);
this.#deferred?.resolve();
}
increment() {
/**
*
* @param {boolean} blocking
*/
increment(blocking) {
this.#pending += 1;
if (blocking) this.#blocking_pending += 1;
}
decrement() {
/**
*
* @param {boolean} blocking
*/
decrement(blocking) {
this.#pending -= 1;
if (blocking) this.#blocking_pending -= 1;
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
@ -391,6 +438,9 @@ export class Batch {
schedule_effect(e);
}
this.#dirty_effects = [];
this.#maybe_dirty_effects = [];
this.flush();
}
@ -561,7 +611,7 @@ function infinite_loop_guard() {
}
}
/** @type {Effect[] | null} */
/** @type {Set<Effect> | null} */
export let eager_block_effects = null;
/**
@ -578,7 +628,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
eager_block_effects = [];
eager_block_effects = new Set();
update_effect(effect);
@ -601,15 +651,34 @@ function flush_queued_effects(effects) {
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.length > 0) {
// TODO this feels incorrect! it gets the tests passing
if (eager_block_effects?.size > 0) {
old_values.clear();
for (const e of eager_block_effects) {
update_effect(e);
// Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
// Run effects in order from ancestor to descendant, else we could run into nullpointers
/** @type {Effect[]} */
const ordered_effects = [e];
let ancestor = e.parent;
while (ancestor !== null) {
if (eager_block_effects.has(ancestor)) {
eager_block_effects.delete(ancestor);
ordered_effects.push(ancestor);
}
ancestor = ancestor.parent;
}
for (let j = ordered_effects.length - 1; j >= 0; j--) {
const e = ordered_effects[j];
// Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
update_effect(e);
}
}
eager_block_effects = [];
eager_block_effects.clear();
}
}
}
@ -623,15 +692,24 @@ function flush_queued_effects(effects) {
* these effects can re-run after another batch has been committed
* @param {Value} value
* @param {Source[]} sources
* @param {Set<Value>} marked
* @param {Map<Reaction, boolean>} checked
*/
function mark_effects(value, sources) {
function mark_effects(value, sources, marked, checked) {
if (marked.has(value)) return;
marked.add(value);
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_effects(/** @type {Derived} */ (reaction), sources);
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) {
mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked);
} else if (
(flags & (ASYNC | BLOCK_EFFECT)) !== 0 &&
(flags & DIRTY) === 0 && // we may have scheduled this one already
depends_on(reaction, sources, checked)
) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
@ -642,20 +720,27 @@ function mark_effects(value, sources) {
/**
* @param {Reaction} reaction
* @param {Source[]} sources
* @param {Map<Reaction, boolean>} checked
*/
function depends_on(reaction, sources) {
function depends_on(reaction, sources, checked) {
const depends = checked.get(reaction);
if (depends !== undefined) return depends;
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)) {
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources, checked)) {
checked.set(/** @type {Derived} */ (dep), true);
return true;
}
}
}
checked.set(reaction, false);
return false;
}

@ -10,7 +10,8 @@ import {
MAYBE_DIRTY,
STALE_REACTION,
UNOWNED,
ASYNC
ASYNC,
WAS_MARKED
} from '#client/constants';
import {
active_reaction,
@ -127,7 +128,17 @@ export function async_derived(fn, location) {
// 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.
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context);
Promise.resolve(fn())
.then(d.resolve, d.reject)
.then(() => {
if (batch === current_batch && batch.committed) {
// if the batch was rejected as stale, we need to cleanup
// after any `$.save(...)` calls inside `fn()`
batch.deactivate();
}
unset_context();
});
} catch (error) {
d.reject(error);
unset_context();
@ -136,17 +147,16 @@ export function async_derived(fn, location) {
if (DEV) current_async_effect = null;
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.is_pending();
if (should_suspend) {
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
if (!pending) {
batch.increment();
batch.increment(blocking);
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
deferreds.set(batch, d);
}
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
deferreds.set(batch, d);
}
/**
@ -156,7 +166,7 @@ export function async_derived(fn, location) {
const handler = (value, error = undefined) => {
current_async_effect = null;
if (!pending) batch.activate();
batch.activate();
if (error) {
if (error !== STALE_REACTION) {
@ -193,7 +203,7 @@ export function async_derived(fn, location) {
if (should_suspend) {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
batch.decrement(blocking);
}
};
@ -317,6 +327,7 @@ export function execute_derived(derived) {
stack.push(derived);
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
@ -326,6 +337,7 @@ export function execute_derived(derived) {
}
} else {
try {
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {

@ -149,6 +149,9 @@ function create_effect(type, fn, sync, push = true) {
(e.f & EFFECT_PRESERVED) === 0
) {
e = e.first;
if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) {
e.f |= EFFECT_TRANSPARENT;
}
}
if (e !== null) {
@ -553,15 +556,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();
});
}
@ -603,7 +607,12 @@ export function pause_children(effect, transitions, local) {
while (child !== null) {
var sibling = child.next;
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
var transparent =
(child.f & EFFECT_TRANSPARENT) !== 0 ||
// If this is a branch effect without a block effect parent,
// it means the parent block effect was pruned. In that case,
// transparency information was transferred to the branch effect.
((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
@ -662,3 +671,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;
}
}

@ -27,7 +27,8 @@ import {
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC
ASYNC,
WAS_MARKED
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -332,11 +333,14 @@ function mark_reactions(signal, status) {
}
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
if ((flags & WAS_MARKED) === 0) {
reaction.f |= WAS_MARKED;
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
}
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {
if (eager_block_effects !== null) {
eager_block_effects.push(/** @type {Effect} */ (reaction));
eager_block_effects.add(/** @type {Effect} */ (reaction));
}
}

@ -20,7 +20,8 @@ import {
DISCONNECTED,
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE
ERROR_VALUE,
WAS_MARKED
} from './constants.js';
import { old_values } from './reactivity/sources.js';
import {
@ -161,6 +162,10 @@ export function is_dirty(reaction) {
var dependencies = reaction.deps;
var is_unowned = (flags & UNOWNED) !== 0;
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if (dependencies !== null) {
var i;
var dependency;

@ -418,15 +418,6 @@ export function ensure_array_like(array_like_or_iterator) {
return [];
}
/**
* @param {any[]} args
* @param {Function} [inspect]
*/
// eslint-disable-next-line no-console
export function inspect(args, inspect = console.log) {
inspect('init', ...args);
}
/**
* @template V
* @param {() => V} get_value

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

@ -197,6 +197,26 @@ export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS)
export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
/**
* @param {any[]} logs
*/
export function normalise_inspect_logs(logs) {
return logs.map((log) => {
if (log instanceof Error) {
const last_line = log.stack
?.trim()
.split('\n')
.filter((line) => !line.includes('at Module.get_stack'))[1];
const match = last_line && /(at .+) /.exec(last_line);
return match && match[1];
}
return log;
});
}
/**
* @param {any[]} logs
*/

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: 'foo'
});

@ -0,0 +1,7 @@
<script>
import Component from './component.svelte';
</script>
<Component let:data>
{@const thing = data}
{thing}
</Component>

@ -6,7 +6,7 @@ export default test({
const [reset, resolve] = target.querySelectorAll('button');
reset.click();
await settled();
await tick();
assert.deepEqual(logs, ['aborted']);
resolve.click();

@ -12,7 +12,7 @@
</script>
<button onclick={() => {
input.focus();
input?.focus();
resolvers.shift()?.();
}}>shift</button>

@ -12,7 +12,7 @@
</script>
<button onclick={() => {
select.focus();
select?.focus();
resolvers.shift()?.();
}}>shift</button>

@ -24,5 +24,7 @@ export default test({
<p>1</p>
`
);
}
},
expect_unhandled_rejections: true
});

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

@ -0,0 +1,36 @@
<script>
let resolvers = [];
function push(value) {
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
let count = $state(0);
</script>
<button onclick={() => count += 1}>{$state.eager(count)}</button>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<svelte:boundary>
{#if await push(count) % 2 === 0}
<p>even</p>
{:else}
<p>odd</p>
{/if}
{#key count}
<svelte:boundary>
<p>{await push(count)}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
{/key}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -1,7 +1,11 @@
<script>
$effect(() => {
console.log('before');
});
await 1;
$effect(() => {
console.log('hello');
console.log('after');
});
</script>

@ -3,7 +3,8 @@ import { test } from '../../test';
export default test({
async test({ assert, logs }) {
assert.deepEqual(logs, []);
await tick();
assert.deepEqual(logs, ['hello']);
assert.deepEqual(logs, ['before', 'after']);
}
});

@ -0,0 +1,5 @@
<script>
$effect(() => {
console.log('in effect')
});
</script>

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [shift] = target.querySelectorAll('button');
await tick();
assert.deepEqual(logs, []);
shift.click();
await tick();
assert.deepEqual(logs, ['in effect']);
}
});

@ -0,0 +1,22 @@
<script>
import Child from './Child.svelte';
let resolvers = [];
function push(value) {
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<svelte:boundary>
<p>{await push('hello')}</p>
<Child />
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -3,23 +3,28 @@ 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) {
function sleep(t = 50) {
return new Promise((r) => setTimeout(r, t));
}
await macrotask();
await sleep();
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
const [input] = target.querySelectorAll('input');
input.value = '1';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask();
await sleep();
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
input.value = '12';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask(6);
await sleep();
assert.htmlEqual(target.innerHTML, '<input> 3 | 12');
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
await sleep();
assert.htmlEqual(target.innerHTML, '<input> 4 | ');
}
});

@ -1,26 +1,31 @@
<script>
let count = $state(0);
let value = $state('');
let prev;
let resolver;
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();
let r = Promise.withResolvers();
function update_and_resolve() {
count++;
r.resolve(v);
}
// make sure the second promise resolve before the first one
if (resolver){
new Promise(r => {
setTimeout(r);
}).then(update_and_resolve).then(() => {
setTimeout(() => {
resolver();
resolver = null;
});
});
} else if (v) {
resolver = update_and_resolve;
} else {
prev = Promise.withResolvers();
prev.promise.then(() => {
count++;
r.resolve(v)
});
Promise.resolve().then(update_and_resolve);
}
return r.promise;

@ -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;

@ -5,6 +5,6 @@ export default test({
dev: true
},
async test({ assert, logs }) {
assert.deepEqual(logs, ['init', 1, 'init', 1]);
assert.deepEqual(logs, [1, 1]);
}
});

@ -0,0 +1,12 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
await tick();
assert.deepEqual(logs, ['attachment']);
}
});

@ -0,0 +1,20 @@
<script>
let fail = $state(false);
function error() {
throw new Error('oops');
}
function attachment() {
console.log('attachment');
}
</script>
<svelte:boundary>
{fail ? error() : 'all good'}
<button onclick={() => fail = true}>fail</button>
{#snippet failed()}
<div {@attach attachment}>oops!</div>
{/snippet}
</svelte:boundary>

@ -0,0 +1,20 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client'],
async test({ target, assert, logs }) {
const button = target.querySelector('button');
button?.click();
flushSync();
button?.click();
flushSync();
button?.click();
flushSync();
button?.click();
flushSync();
assert.deepEqual(logs, ['two', 'one', 'two', 'one', 'two']);
}
});

@ -0,0 +1,18 @@
<script lang="ts">
let b = $state(false);
let v = $state("two");
$effect(() => {
v = b ? "one" : "two";
})
</script>
<button onclick={() => b = !b}>Trigger</button>
{#if v === "one"}
<div>if1 matched! {console.log('one')}</div>
{:else if v === "two"}
<div>if2 matched! {console.log('two')}</div>
{:else}
<div>nothing matched {console.log('else')}</div>
{/if}

@ -0,0 +1,13 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client'],
async test({ target, assert }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.equal(target.textContent?.trim(), 'Trigger');
}
});

@ -0,0 +1,18 @@
<script>
let centerRow = $state({ nested: { optional: 2, required: 3 } });
let someChange = $state(false);
$effect(() => {
if (someChange) centerRow = undefined;
});
</script>
{#if centerRow?.nested}
{#if centerRow?.nested?.optional != undefined && centerRow.nested.optional > 0}
op: {centerRow.nested.optional}<br />
{:else}
req: {centerRow.nested.required}<br />
{/if}
{/if}
<button onclick={() => (someChange = true)}>Trigger</button>

@ -0,0 +1,6 @@
<script>
let { p } = $props();
$effect.pre(() => {
console.log('running ' + p)
})
</script>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
const button = target.querySelector('button');
button?.click();
flushSync();
assert.deepEqual(logs, ['pre', 'running b', 'pre', 'pre']);
}
});

@ -0,0 +1,18 @@
<script>
import Component from './Component.svelte';
let p = $state('b');
$effect.pre(() => {
console.log('pre')
if (p === 'a') p = null;
})
</script>
{#if p || !p}
{#if p}
<Component {p} />
{/if}
{/if}
<button onclick={() => p = 'a'}>a</button>

@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -13,6 +14,6 @@ export default test({
button?.click();
});
assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]);
assert.deepEqual(normalise_inspect_logs(logs), [[1, 2, 3, 7], [2, 3, 7], 'at Object.doSplice']);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -6,6 +7,6 @@ export default test({
},
async test({ assert, logs }) {
assert.deepEqual(logs, ['init', undefined, 'update', [{}]]);
assert.deepEqual(normalise_inspect_logs(logs), [undefined, [{}], 'at $effect']);
}
});

@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -14,8 +15,7 @@ export default test({
});
assert.htmlEqual(target.innerHTML, `<button>update</button>\n1`);
assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
{
data: {
derived: 0,
@ -23,14 +23,14 @@ export default test({
},
derived: []
},
'update',
{
data: {
derived: 0,
list: [1]
},
derived: [1]
}
},
'at HTMLButtonElement.Main.button.__click'
]);
}
});

@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -13,22 +14,19 @@ export default test({
button?.click();
});
assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
'0',
true,
'init',
'1',
false,
'init',
'2',
false,
'update',
'0',
false,
'update',
'at $effect',
'1',
true
true,
'at $effect'
]);
}
});

@ -1,5 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_inspect_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -12,15 +13,13 @@ export default test({
btn2.click();
flushSync();
assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
new Map(),
'init',
new Set(),
'update',
new Map([['a', 'a']]),
'update',
new Set(['a'])
'at SvelteMap.set',
new Set(['a']),
'at SvelteSet.add'
]);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -11,6 +12,15 @@ export default test({
b2.click();
await Promise.resolve();
assert.deepEqual(logs, ['init', 0, 0, 'update', 1, 0, 'update', 1, 1]);
assert.deepEqual(normalise_inspect_logs(logs), [
0,
0,
1,
0,
'at HTMLButtonElement.<anonymous>',
1,
1,
'at HTMLButtonElement.<anonymous>'
]);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -6,6 +7,6 @@ export default test({
},
async test({ assert, logs }) {
assert.deepEqual(logs, ['init', 0, 'update', 1]);
assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at $effect']);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -10,13 +11,12 @@ export default test({
b1.click();
await Promise.resolve();
assert.deepEqual(logs, [
'init',
assert.deepEqual(normalise_inspect_logs(logs), [
{ x: { count: 0 } },
[{ count: 0 }],
'update',
{ x: { count: 1 } },
[{ count: 1 }]
[{ count: 1 }],
'at HTMLButtonElement.<anonymous>'
]);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -10,6 +11,13 @@ export default test({
btn.click();
await Promise.resolve();
assert.deepEqual(logs, ['init', {}, 'init', [], 'update', { x: 'hello' }, 'update', ['hello']]);
assert.deepEqual(normalise_inspect_logs(logs), [
{},
[],
{ x: 'hello' },
'at HTMLButtonElement.on_click',
['hello'],
'at HTMLButtonElement.on_click'
]);
}
});

@ -18,6 +18,6 @@ export default test({
};
b.a.b = b;
assert.deepEqual(logs, ['init', a, 'init', b]);
assert.deepEqual(logs, [a, b]);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -11,6 +12,12 @@ export default test({
btn.click();
await Promise.resolve();
assert.deepEqual(logs, ['init', [], 'update', [{}], 'update', [{}, {}]]);
assert.deepEqual(normalise_inspect_logs(logs), [
[],
[{}],
'at HTMLButtonElement.on_click',
[{}, {}],
'at HTMLButtonElement.on_click'
]);
}
});

@ -1,3 +1,4 @@
import { normalise_inspect_logs } from '../../../helpers.js';
import { test } from '../../test';
export default test({
@ -11,6 +12,6 @@ export default test({
b2.click();
await Promise.resolve();
assert.deepEqual(logs, ['init', 0, 'update', 1]);
assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.<anonymous>']);
}
});

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>Toggle</button>
<div>Should not transition out</div>
`
);
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>Toggle</button>');
}
});

@ -0,0 +1,18 @@
<script>
import { slide } from 'svelte/transition';
let showText = $state(false);
let show = $state(true);
</script>
<button onclick={() => showText = !showText}>
Toggle
</button>
{#if showText}
{#if show}
<div transition:slide>
Should not transition out
</div>
{/if}
{/if}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ compileOptions: { experimental: { async: true } } });

@ -0,0 +1,52 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
export default function Async_in_derived($$anchor, $$props) {
$.push($$props, true);
$.async_body($$anchor, async ($$anchor) => {
let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
let no1 = $.derived(async () => {
return await 1;
});
let no2 = $.derived(() => async () => {
return await 1;
});
if ($.aborted()) return;
var fragment = $.comment();
var node = $.first_child(fragment);
{
var consequent = ($$anchor) => {
$.async_body($$anchor, async ($$anchor) => {
const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
const no1 = $.derived(() => (async () => {
return await 1;
})());
const no2 = $.derived(() => (async () => {
return await 1;
})());
if ($.aborted()) return;
});
};
$.if(node, ($$render) => {
if (true) $$render(consequent);
});
}
$.append($$anchor, fragment);
});
$.pop();
}

@ -0,0 +1,40 @@
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_in_derived($$renderer, $$props) {
$$renderer.component(($$renderer) => {
$$renderer.async(async ($$renderer) => {
let yes1 = (await $.save(1))();
let yes2 = foo((await $.save(1))());
let no1 = (async () => {
return await 1;
})();
let no2 = async () => {
return await 1;
};
$$renderer.async(async ($$renderer) => {
if (true) {
$$renderer.push('<!--[-->');
const yes1 = (await $.save(1))();
const yes2 = foo((await $.save(1))());
const no1 = (async () => {
return await 1;
})();
const no2 = (async () => {
return await 1;
})();
} else {
$$renderer.push('<!--[!-->');
}
});
$$renderer.push(`<!--]-->`);
});
});
}

@ -0,0 +1,21 @@
<script>
let yes1 = $derived(await 1);
let yes2 = $derived(foo(await 1));
let no1 = $derived.by(async () => {
return await 1;
});
let no2 = $derived(async () => {
return await 1;
});
</script>
{#if true}
{@const yes1 = await 1}
{@const yes2 = foo(await 1)}
{@const no1 = (async () => {
return await 1;
})()}
{@const no2 = (async () => {
return await 1;
})()}
{/if}

@ -21,7 +21,7 @@
"polka": "^1.0.0-next.25",
"svelte": "workspace:*",
"tinyglobby": "^0.2.12",
"vite": "^7.1.5",
"vite": "^7.1.11",
"vite-plugin-devtools-json": "^1.0.0",
"vite-plugin-inspect": "^11.3.3"
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save