Merge remote-tracking branch 'origin/main' into goodbye-acorn

# Conflicts:
#	packages/svelte/src/compiler/phases/1-parse/index.js
#	packages/svelte/src/compiler/phases/1-parse/state/element.js
#	packages/svelte/src/compiler/phases/1-parse/state/tag.js
#	packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
#	packages/svelte/src/compiler/phases/3-transform/utils.js
#	packages/svelte/src/compiler/phases/scope.js
goodbye-acorn
Nic 3 weeks ago
commit 05bba23563

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve global CSS in components without scopable elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reduce SSR render result garbage collection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resolve the fallback of an each block in the enclosing scope

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent effect tree of batches from interfering with each other

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove `WAS_MARKED` flag in favor of `Set`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep `$state.eager` when used as a variable initializer

@ -13,7 +13,7 @@ import {
sbench_create_4to1,
sbench_create_signals
} from './sbench.js';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { create_test } from './util.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
@ -39,7 +39,8 @@ for (const file of fs.readdirSync(`${dirname}/tests`)) {
const name = file.replace('.bench.js', '');
const module = await import(`${dirname}/tests/${file}`);
const module_url = pathToFileURL(path.join(dirname, 'tests', file));
const module = await import(module_url.href);
const { owned, unowned } = create_test(name, module.default);
reactivity_benchmarks.push(owned, unowned);

@ -25,12 +25,12 @@ export const wrapper_bench = {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
render(App);
render(App).body;
}
return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
render(App);
render(App).body;
}
});
}

@ -145,6 +145,19 @@ export function prune(stylesheet, elements) {
},
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
const rule = /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule);
const first = selectors[0]?.selectors[0];
const is_icss_export =
selectors.length === 1 &&
selectors[0].selectors.length === 1 &&
first?.type === 'PseudoClassSelector' &&
first.name === 'export' &&
first.args === null;
// Global and ICSS export rules do not depend on an element in this component
if (every_is_global(selectors, 0, selectors.length, rule) || is_icss_export) {
node.metadata.used = true;
}
for (const element of elements) {
seen.clear();
@ -153,12 +166,7 @@ export function prune(stylesheet, elements) {
// Elements rendered through <svelte:head> are not style-scopable.
// Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>.
!is_inside_svelte_head(element) &&
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element,
BACKWARD
)
apply_selector(selectors, rule, element, BACKWARD)
) {
node.metadata.used = true;
}

@ -15,7 +15,7 @@ import {
} from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';

@ -1,7 +1,7 @@
/** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { validate_identifier_name } from './shared/utils.js';
import { validate_identifier_name } from '../../scope.js';
/**
* @param {ClassDeclaration} node

@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
import { validate_identifier_name } from './shared/utils.js';
import { validate_identifier_name } from '../../scope.js';
/**
* @param {FunctionDeclaration} node

@ -1,12 +1,11 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import { should_proxy } from '../../3-transform/client/utils.js';
import { should_proxy, get_rune } from '../../scope.js';
import { is_reference } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**

@ -1,8 +1,8 @@
/** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
import { get_rune, validate_identifier_name } from '../../scope.js';
import { ensure_no_module_import_conflict } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';

@ -246,44 +246,6 @@ export function is_pure(node, context) {
return false;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}
/**
* Checks that the exported name is not a derived or reassigned state variable.
* @param {Node} node

@ -127,44 +127,6 @@ export function is_prop_source(binding, state) {
);
}
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/**
* An async thunk. If an `await` inside restores the reaction context via `$.save`,
* the body exits through `$.unsave` so the context cannot leak into foreign microtasks

@ -9,10 +9,10 @@ import {
is_expression_async
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { build_getter, should_proxy } from '../utils.js';
import { build_getter } from '../utils.js';
import { should_proxy, get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**

@ -2,8 +2,7 @@
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_rune, should_proxy } from '../../../scope.js';
import { get_inspect_args } from '../../utils.js';
/**

@ -5,14 +5,8 @@ import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import {
async_thunk,
get_prop_source,
is_prop_source,
is_state_source,
should_proxy
} from '../utils.js';
import { get_rune, should_proxy } from '../../../scope.js';
import { async_thunk, get_prop_source, is_prop_source, is_state_source } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -35,6 +29,7 @@ export function VariableDeclaration(node, context) {
rune === '$inspect' ||
rune === '$inspect.trace' ||
rune === '$state.snapshot' ||
rune === '$state.eager' ||
rune === '$host'
) {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));

@ -9,7 +9,7 @@ import {
import * as e from '../../errors.js';
import { walk } from 'zimmerframe';
import { extract_identifiers, is_reference } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import { set_scope } from '../scope.js';
/**

@ -17,7 +17,6 @@ import {
import { is_reserved, is_rune } from '../../utils.js';
import { is_template_node } from './1-parse/index.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
@ -1426,6 +1425,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
EachBlock(node, { state, visit }) {
visit(node.expression);
if (node.fallback) visit(node.fallback);
// context and children are a new scope
const scope = state.scope.child();
@ -1471,7 +1471,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
for (const child of node.body.nodes) {
visit(child, { scope });
}
if (node.fallback) visit(node.fallback, { scope });
node.metadata = {
expression: new ExpressionMetadata(),
@ -1676,3 +1675,79 @@ function get_global_keypath(node, scope) {
return n.name + joined;
}
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}

@ -1,10 +1,10 @@
/** @import { ValidatedCompileOptions } from '#compiler' */
/** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */
import remapping from '@jridgewell/remapping';
import { push_array } from './push_array.js';
/**
* @param {string} s
@ -12,6 +12,18 @@ import { push_array } from './push_array.js';
function last_line_length(s) {
return s.length - s.lastIndexOf('\n') - 1;
}
/**
* `array.push(...items)` overflows the stack for large `items`.
* @template T
* @param {T[]} array
* @param {T[]} items
*/
function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}
// mutate map in-place
/**

@ -1,13 +0,0 @@
/**
* Pushes all `items` into `array` using `push`, therefore mutating the array.
* We do this for memory and perf reasons, and because `array.push(...items)` would
* run into a "max call stack size exceeded" error with too many items (~65k).
* @template T
* @param {T[]} array
* @param {T[]} items
*/
export function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}

@ -51,15 +51,6 @@ export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
export const EFFECT_OFFSCREEN = 1 << 25;
// Flags exclusive to deriveds
/**
* 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). This is a pure performance optimization flag and
* should not be used for any other purpose!
*/
export const WAS_MARKED = 1 << 16;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;

@ -15,7 +15,6 @@ import {
MAYBE_DIRTY,
RENDER_EFFECT,
ROOT_EFFECT,
WAS_MARKED,
MANAGED_EFFECT
} from '#client/constants';
import { snapshot } from '../../shared/clone.js';
@ -204,7 +203,6 @@ export function log_reactions(signal) {
if ((flags & DIRTY) !== 0) names.push('DIRTY');
if ((flags & MAYBE_DIRTY) !== 0) names.push('MAYBE_DIRTY');
if ((flags & CONNECTED) !== 0) names.push('CONNECTED');
if ((flags & WAS_MARKED) !== 0) names.push('WAS_MARKED');
if ((flags & INERT) !== 0) names.push('INERT');
if ((flags & DESTROYED) !== 0) names.push('DESTROYED');

@ -22,7 +22,6 @@ import {
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
import {
active_effect,
active_reaction,
get,
increment_write_version,
@ -38,7 +37,6 @@ import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
import { log_effect_tree } from '../dev/debug.js';
import { OBSOLETE } from './deriveds.js';
@ -160,10 +158,17 @@ export class Batch {
#deferred = null;
/**
* The root effects that need to be flushed
* Effects that were scheduled in this batch but not yet 'resolved' into the
* root effects that need to be flushed. Resolving — the upwards traversal that
* marks the path to each effect on the shared effect tree (see #resolve) — is
* deferred until the batch is processed, so that the markers are created and
* consumed within a single traversal. Scheduling into other batches (which can
* happen concurrently, e.g. while a batch is committed) can therefore never
* observe (and be confused by) this batch's markers.
* May contain duplicates — deduplication happens during resolving
* @type {Effect[]}
*/
#roots = [];
#scheduled = [];
/**
* Effects created while this batch was active.
@ -273,14 +278,57 @@ export class Batch {
this.#unskipped_branches.add(effect);
}
#process() {
this.#started = true;
/**
* Convert the effects that were scheduled in this batch into the root effects
* that need to be traversed, marking the path to each effect (by clearing the
* `CLEAN` flag on ancestor branches) so that the traversal can find them.
* This happens right before traversal rather than at scheduling time, so that
* the markers left on the (shared) effect tree are created and consumed within
* a single traversal — scheduling into other batches can never observe them
* @returns {Effect[]}
*/
#resolve() {
/** @type {Effect[]} */
var roots = [];
if (flush_count++ > 1000) {
this.#unlink();
infinite_loop_guard();
for (const effect of this.#scheduled) {
// skip effects that are destroyed, or that already ran (e.g. because
// they were reached by the traversal that preceded a drain iteration,
// or because they were scheduled twice)
if ((effect.f & DESTROYED) !== 0 || (effect.f & (DIRTY | MAYBE_DIRTY)) === 0) continue;
var e = effect;
var covered = false;
while (e.parent !== null) {
e = e.parent;
var flags = e.f;
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// the path to the root was already marked, meaning the
// root was already collected — nothing left to do
covered = true;
break;
}
e.f ^= CLEAN;
}
}
if (!covered) {
roots.push(e);
}
}
this.#scheduled = [];
return roots;
}
#process() {
this.#started = true;
if (DEV) {
// track all the values that were updated during this flush,
// so that they can be reset afterwards
@ -304,9 +352,6 @@ export class Batch {
this.schedule(e);
}
const roots = this.#roots;
this.#roots = [];
this.apply();
/** @type {Effect[]} */
@ -321,18 +366,28 @@ export class Batch {
*/
var updates = (legacy_updates = []);
for (const root of roots) {
try {
this.#traverse(root, effects, render_effects);
} catch (e) {
reset_all(root);
// If there's no async work left, this branch is now dead and needs
// to be discarded to not become a zombie that is never cleaned up.
// See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
// for a (non-minimal) reproduction that demonstrates a case where this is necessary
// to not get follow-up false-positives via "batch has scheduled roots" invariant errors.
if (!this.#is_deferred()) this.discard();
throw e;
// Effects can be scheduled during traversal (e.g. because a parent each/await/etc
// block updated an internal source, or because an effect invalidated itself)
// hence we loop until there are no more scheduled effects.
while (this.#scheduled.length > 0) {
if (flush_count++ > 1000) {
this.#unlink();
infinite_loop_guard(); // TODO try to reset_all() here?
}
for (const root of this.#resolve()) {
try {
this.#traverse(root, effects, render_effects);
} catch (e) {
reset_all(root);
// If there's no async work left, this branch is now dead and needs
// to be discarded to not become a zombie that is never cleaned up.
// See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
// for a (non-minimal) reproduction that demonstrates a case where this is necessary
// to not get follow-up false-positives via "batch has scheduled roots" invariant errors.
if (!this.#is_deferred()) this.discard();
throw e;
}
}
}
@ -393,7 +448,7 @@ export class Batch {
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
if (this.#pending === 0 && (this.#roots.length === 0 || next_batch !== null)) {
if (this.#pending === 0 && (this.#scheduled.length === 0 || next_batch !== null)) {
this.#unlink();
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
@ -408,12 +463,15 @@ export class Batch {
}
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// causing an effect to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
if (this.#scheduled.length > 0) {
if (next_batch !== null) {
const batch = next_batch;
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
for (const e of this.#scheduled) {
next_batch.#scheduled.push(e);
}
this.#scheduled = [];
} else {
next_batch = this;
}
@ -712,7 +770,7 @@ export class Batch {
// The microtask queue can contain the batch already scheduled to run right
// after this one is finished, so throwing the invariant would be wrong here.
if (DEV && !batch.#decrement_queued) {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
invariant(batch.#scheduled.length === 0, 'Batch has scheduled effects');
}
// A batch was unskipped in a later batch -> tell prior batches to unskip it, too
@ -768,14 +826,12 @@ export class Batch {
// Only apply and traverse when we know we triggered async work with marking the effects
// and know this won't run anyway right afterwards
if (batch.#roots.length > 0 && !batch.#decrement_queued) {
if (batch.#scheduled.length > 0 && !batch.#decrement_queued) {
batch.apply();
for (var root of batch.#roots) {
for (var root of batch.#resolve()) {
batch.#traverse(root, [], []);
}
batch.#roots = [];
}
batch.deactivate();
@ -939,43 +995,7 @@ export class Batch {
return;
}
var e = effect;
while (e.parent !== null) {
e = e.parent;
var flags = e.f;
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, or because a branch is being unskipped,
// bail out or we'll cause a second flush
if (collected_effects !== null && e === active_effect) {
if (async_mode_flag) return;
// in sync mode, render effects run during traversal. in an extreme edge case
// — namely that we're setting a value inside a derived read during traversal —
// they can be made dirty after they have already been visited, in which
// case we shouldn't bail out. we also shouldn't bail out if we're
// updating a store inside a `$:`, since this might invalidate
// effects that were already visited
if (
(active_reaction === null || (active_reaction.f & DERIVED) === 0) &&
!legacy_is_updating_store
) {
return;
}
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// branch is already dirty, bail
return;
}
e.f ^= CLEAN;
}
}
this.#roots.push(e);
this.#scheduled.push(effect);
}
#unlink() {

@ -9,7 +9,6 @@ import {
EFFECT_PRESERVED,
STALE_REACTION,
ASYNC,
WAS_MARKED,
DESTROYED,
CLEAN,
REACTION_RAN,
@ -365,7 +364,6 @@ export function execute_derived(derived) {
stack.push(derived);
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
@ -375,7 +373,6 @@ export function execute_derived(derived) {
}
} else {
try {
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {

@ -25,10 +25,7 @@ import {
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC,
WAS_MARKED,
CONNECTED,
REACTION_IS_UPDATING
ASYNC
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -171,6 +168,17 @@ export function set(source, value, should_proxy = false) {
return internal_set(source, new_value, legacy_updates);
}
/**
* A set of signals we have already seen while traversing in mark_reactions.
* Not always set to balance the common case of sources only having a couple
* of (transitive) dependencies (where always creating a Set would be bad for perf)
* with the edge case of extremely deep or wide dependency arrays with cycles.
* @type {Set<any> | null}
*/
var seen = null;
/** Number of transitive dependencies, see {@link seen} for more info */
var count_deps = 0;
/**
* @template V
* @param {Source<V>} source
@ -240,7 +248,10 @@ export function internal_set(source, value, updated_during_traversal = null) {
// For debugging, in case you want to know which reactions are being scheduled:
// log_reactions(source);
seen = null;
count_deps = 0;
mark_reactions(source, DIRTY, updated_during_traversal);
seen = null;
// It's possible that the current reaction might not have up-to-date dependencies
// whilst it's actively running. So in the case of ensuring it registers the reaction
@ -347,6 +358,18 @@ function mark_reactions(signal, status, updated_during_traversal) {
var runes = is_runes();
var length = reactions.length;
count_deps += length;
// Activate the `seen` Set if we think from the unusually high number of deps that
// there might be cycles in the graph, to avoid repeated lookups for reactions
// Example: https://github.com/sveltejs/svelte/issues/16658 has a graph with one source
// reaching ~10000 distinct deriveds/effects each, resulting in 65 million walks through repeated visits.
if (count_deps > 100000 && seen === null) seen = new Set();
if (seen !== null) {
if (seen.has(signal)) return;
seen.add(signal);
}
for (var i = 0; i < length; i++) {
var reaction = reactions[i];
var flags = reaction.f;
@ -370,18 +393,7 @@ function mark_reactions(signal, status, updated_during_traversal) {
var derived = /** @type {Derived} */ (reaction);
batch_values?.delete(derived);
if ((flags & WAS_MARKED) === 0) {
// Only connected deriveds being executed outside the update cycle can be reliably unmarked right away
if (
flags & CONNECTED &&
(active_effect === null || (active_effect.f & REACTION_IS_UPDATING) === 0)
) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY, updated_during_traversal);
}
mark_reactions(derived, MAYBE_DIRTY, updated_during_traversal);
} else if (not_dirty) {
var effect = /** @type {Effect} */ (reaction);

@ -1,24 +1,7 @@
/** @import { Derived, Effect, Value } from '#client' */
import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY, WAS_MARKED } from '#client/constants';
/** @import { Effect } from '#client' */
import { CLEAN, DIRTY, MAYBE_DIRTY } from '#client/constants';
import { set_signal_status } from './status.js';
/**
* @param {Value[] | null} deps
*/
function clear_marked(deps) {
if (deps === null) return;
for (const dep of deps) {
if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
continue;
}
dep.f ^= WAS_MARKED;
clear_marked(/** @type {Derived} */ (dep).deps);
}
}
/**
* @param {Effect} effect
* @param {Set<Effect>} dirty_effects
@ -31,10 +14,6 @@ export function defer_effect(effect, dirty_effects, maybe_dirty_effects) {
maybe_dirty_effects.add(effect);
}
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
// so that other batches can correctly reach these effects during their own traversal
clear_marked(effect.deps);
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(effect, CLEAN);
}

@ -21,7 +21,6 @@ import {
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE,
WAS_MARKED,
MANAGED_EFFECT,
REACTION_RAN
} from './constants.js';
@ -164,10 +163,6 @@ export function is_dirty(reaction) {
return true;
}
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = /** @type {Value[]} */ (reaction.deps);
var length = dependencies.length;
@ -408,11 +403,8 @@ function remove_reaction(signal, dependency) {
) {
var derived = /** @type {Derived} */ (dependency);
// If we are working with a derived that is owned by an effect, then mark it as being
// disconnected and remove the mark flag, as it cannot be reliably removed otherwise
if ((derived.f & CONNECTED) !== 0) {
derived.f ^= CONNECTED;
derived.f &= ~WAS_MARKED;
}
// In a fork it's possible that a derived is executed and gets reactions, then commits, but is

@ -23,6 +23,87 @@ import { escape_html } from '../../escaping.js';
* @typedef {string | Renderer} RendererItem
*/
class RenderResult {
/** @type {() => AccumulatedContent} */
#render;
/** @type {() => Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>} */
#render_async;
/** @type {AccumulatedContent | undefined} */
#sync;
/** @type {{ script: '' }} */
#hashes = { script: '' };
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */
#promise;
/**
* @param {() => AccumulatedContent} render
* @param {() => Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>} render_async
*/
constructor(render, render_async) {
this.#render = render;
this.#render_async = render_async;
}
#get() {
return (this.#sync ??= this.#render());
}
get html() {
return this.#get().body;
}
get head() {
return this.#get().head;
}
get body() {
return this.#get().body;
}
get hashes() {
return this.#hashes;
}
/**
* This is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param {(value: SyncRenderOutput) => TResult1} onfulfilled
* @param {(reason: unknown) => TResult2} onrejected
*/
then(onfulfilled, onrejected) {
if (!async_mode_flag) {
const result = this.#get();
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body,
hashes: { script: [] }
});
return Promise.resolve(user_result);
}
this.#promise ??= this.#render_async().then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
get: () => {
e.html_deprecated();
}
});
return result;
});
return this.#promise.then(
(result) => onfulfilled(/** @type {SyncRenderOutput} */ (result)),
onrejected
);
}
}
/**
* Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents
* work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the
@ -534,73 +615,17 @@ export class Renderer {
* @returns {RenderOutput}
*/
static render(component, options = {}) {
/** @type {AccumulatedContent | undefined} */
let sync;
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */
let async;
const result = /** @type {RenderOutput} */ ({});
// making these properties non-enumerable so that console.logging
// doesn't trigger a sync render
Object.defineProperties(result, {
html: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
head: {
get: () => {
return (sync ??= Renderer.#render(component, options)).head;
}
},
body: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
hashes: {
value: {
script: ''
}
},
then: {
value:
/**
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
* @param { (reason: unknown) => TResult2 } onrejected
*/
(onfulfilled, onrejected) => {
if (!async_mode_flag) {
const result = (sync ??= Renderer.#render(component, options));
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body,
hashes: { script: [] }
});
return Promise.resolve(user_result);
}
async ??= init_render_context().then(() =>
return /** @type {RenderOutput} */ (
/** @type {unknown} */ (
new RenderResult(
() => Renderer.#render(component, options),
() =>
init_render_context().then(() =>
with_render_context(() => Renderer.#render_async(component, options))
);
return async.then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
get: () => {
e.html_deprecated();
}
});
return onfulfilled(/** @type {SyncRenderOutput} */ (result));
}, onrejected);
}
}
});
return result;
)
)
)
);
}
/**

@ -4,6 +4,16 @@ import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { getAbortSignal } from './abort-signal.js';
test('render result properties are inherited', () => {
const result = Renderer.render((() => {}) as unknown as Component);
expect(Object.hasOwn(result, 'head')).toBe(false);
expect(Object.hasOwn(result, 'body')).toBe(false);
expect(Object.hasOwn(result, 'html')).toBe(false);
expect(Object.hasOwn(result, 'hashes')).toBe(false);
expect(Object.hasOwn(result, 'then')).toBe(false);
});
test('collects synchronous body content by default', () => {
const component = (renderer: Renderer) => {
renderer.push('a');

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

@ -0,0 +1,2 @@
:export { foo: red; }
:is(td, th) { color: red; }

@ -0,0 +1,6 @@
<svelte:head><meta name="x" content="y" /></svelte:head>
<style>
:export { foo: red; }
:is(:global(td), :global(th)) { color: red; }
</style>

@ -0,0 +1,41 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>a</button> <button>b</button> <button>shift</button> <button>pop</button>';
export default test({
async test({ assert, target }) {
await tick();
const [a, b, , pop] = target.querySelectorAll('button');
const shift = target.querySelectorAll('button')[2];
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>a</p><p>aa</p><p>1</p>`);
// start two independent batches, both blocked on their awaited expression
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>a</p><p>aa</p><p>1</p>`);
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>a</p><p>aa</p><p>1</p>`);
// resolve the newer (b) batch first. Committing it must not commit the
// still-pending `a` batch, whose async work has not completed — `a` must
// still read 'a', and the unrelated `c` update must not be blocked
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>b</p><p>ab</p><p>2</p>`);
// stale promise from the `a` batch's first run — resolving it does nothing
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>b</p><p>ab</p><p>2</p>`);
// the `a` batch's re-run await ('bb') resolves — everything is committed
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons} <p>b</p><p>b</p><p>bb</p><p>2</p>`);
}
});

@ -0,0 +1,38 @@
<script>
let a = $state('a');
let b = $state('a');
let c = $state(0);
let n = 0;
let queued = [];
let first = true;
function push(v) {
if (first) {
first = false;
return v;
}
return new Promise((resolve) => {
queued.push(() => resolve(v));
});
}
// when a batch changing `b` commits, this writes `c` during that batch's
// flush phase — the write lands in a freshly-created batch
$effect(() => {
b;
c = ++n;
});
</script>
<button onclick={() => a = 'b'}>a</button>
<button onclick={() => b = 'b'}>b</button>
<button onclick={() => queued.shift()?.()}>shift</button>
<button onclick={() => queued.pop()?.()}>pop</button>
<p>{a}</p>
<p>{b}</p>
<p>{await push(a + b)}</p>
<p>{c}</p>

@ -0,0 +1,14 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button>outer</button>',
test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => {
button?.click();
});
assert.htmlEqual(target.innerHTML, '<button>changed</button>');
}
});

@ -0,0 +1,10 @@
<script>
let items = $state([]);
let item = $state('outer');
</script>
{#each items as item}
<p>{item}</p>
{:else}
<button onclick={() => (item = 'changed')}>{item}</button>
{/each}

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

@ -0,0 +1,7 @@
<script>
let n = $state(2);
let d = $derived(n * 10);
let x = $state.eager(d);
</script>
<p>{x}</p>
Loading…
Cancel
Save