From a9ac217b8f11b22adc570115dabc8f9b6a561993 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 10:34:39 -0400 Subject: [PATCH 01/26] WIP --- .../internal/client/dom/blocks/branches.js | 103 ++++++++++++++++++ .../src/internal/client/dom/blocks/if.js | 68 +----------- 2 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 packages/svelte/src/internal/client/dom/blocks/branches.js diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js new file mode 100644 index 000000000000..e79e34a06809 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -0,0 +1,103 @@ +/** @import { Effect, TemplateNode } from '#client' */ +import { Batch, current_batch } from '../../reactivity/batch.js'; +import { branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { create_text, should_defer_append } from '../operations.js'; + +/** + * @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch + */ + +/** + * @template Key + */ +export class BranchManager { + /** @type {TemplateNode} */ + #anchor; + + /** @type {Map} */ + #batches = new Map(); + + /** @type {Map} */ + #onscreen = new Map(); + + /** @type {Map} */ + #offscreen = new Map(); + + /** + * + * @param {TemplateNode} anchor + */ + constructor(anchor) { + this.#anchor = anchor; + } + + #commit = () => { + var batch = /** @type {Batch} */ (current_batch); + 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) throw new Error('This should never happen!'); + + 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); + } + + this.#batches.delete(batch); + + for (const [k, e] of this.#onscreen) { + if (k === key) continue; + + pause_effect(e, () => { + // TODO if this needed by a pending batch, move the effect to + // a fragment and put it on this.#offscreen + + this.#onscreen.delete(k); + }); + } + }; + + /** + * + * @param {any} key + * @param {(target: TemplateNode) => void} fn + */ + ensure(key, fn) { + if (should_defer_append()) { + var batch = /** @type {Batch} */ (current_batch); + + if (!this.#onscreen.has(key) && !this.#offscreen.has(key)) { + var fragment = document.createDocumentFragment(); + var target = create_text(); + + fragment.append(target); + + this.#offscreen.set(key, { + effect: branch(() => fn(target)), + fragment + }); + } + + this.#batches.set(batch, key); + + batch.add_callback(this.#commit); + } else { + this.#onscreen.set( + key, + branch(() => fn(this.#anchor)) + ); + } + } +} diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 6349ab839931..ba0d2a6d963e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -14,6 +14,8 @@ import { block, branch, pause_effect, resume_effect } from '../../reactivity/eff 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 { BranchManager } from './branches.js'; +import { noop } from '../../../shared/utils.js'; // TODO reinstate https://github.com/sveltejs/svelte/pull/15250 @@ -30,12 +32,6 @@ export function if_block(node, fn, elseif = false) { 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; @@ -48,42 +44,12 @@ export function if_block(node, fn, elseif = false) { 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; - } - }); - } - } + var branches = new BranchManager(anchor); 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; @@ -101,33 +67,7 @@ export function if_block(node, fn, elseif = false) { } } - var defer = should_defer_append(); - var target = anchor; - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - } - - if (condition) { - consequent_effect ??= fn && branch(() => fn(target)); - } else { - alternate_effect ??= fn && branch(() => fn(target)); - } - - if (defer) { - var batch = /** @type {Batch} */ (current_batch); - - var active = condition ? consequent_effect : alternate_effect; - var inactive = condition ? alternate_effect : consequent_effect; - - if (active) batch.skipped_effects.delete(active); - if (inactive) batch.skipped_effects.add(inactive); - - batch.add_callback(commit); - } else { - commit(); - } + branches.ensure(new_condition, fn ?? noop); if (mismatch) { // continue in hydration mode From 39ea5e4c343a055b3f6cda40186b1c8cd06d75a6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 11:18:47 -0400 Subject: [PATCH 02/26] WIP --- .../internal/client/dom/blocks/boundary.js | 26 ++++---------- .../internal/client/dom/blocks/branches.js | 35 +++++++++++++++---- .../src/internal/client/dom/blocks/if.js | 7 ++-- .../src/internal/client/reactivity/effects.js | 22 ++++++++++-- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ca6437e3841f..026ffb36fc51 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -8,7 +8,13 @@ import { import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { handle_error, invoke_error_boundary } from '../../error-handling.js'; -import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { + block, + branch, + destroy_effect, + move_effect, + pause_effect +} from '../../reactivity/effects.js'; import { active_effect, active_reaction, @@ -425,24 +431,6 @@ export class Boundary { } } -/** - * - * @param {Effect} effect - * @param {DocumentFragment} fragment - */ -function move_effect(effect, fragment) { - var node = effect.nodes_start; - var end = effect.nodes_end; - - while (node !== null) { - /** @type {TemplateNode | null} */ - var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - fragment.append(node); - node = next; - } -} - export function get_boundary() { return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b); } diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index e79e34a06809..9adb3ddec93a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -1,6 +1,12 @@ /** @import { Effect, TemplateNode } from '#client' */ import { Batch, current_batch } from '../../reactivity/batch.js'; -import { branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { + branch, + destroy_effect, + move_effect, + pause_effect, + resume_effect +} from '../../reactivity/effects.js'; import { create_text, should_defer_append } from '../operations.js'; /** @@ -57,15 +63,30 @@ export class BranchManager { this.#batches.delete(batch); - for (const [k, e] of this.#onscreen) { + for (const [k, effect] of this.#onscreen) { if (k === key) continue; - pause_effect(e, () => { - // TODO if this needed by a pending batch, move the effect to - // a fragment and put it on this.#offscreen + pause_effect( + effect, + () => { + const keys = Array.from(this.#batches.values()); - this.#onscreen.delete(k); - }); + 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); + }, + false + ); } }; diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index ba0d2a6d963e..23a4555e5bac 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,5 +1,4 @@ -/** @import { Effect, TemplateNode } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ +/** @import { TemplateNode } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, @@ -10,10 +9,8 @@ import { set_hydrate_node, set_hydrating } from '../hydration.js'; -import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { block } 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 { BranchManager } from './branches.js'; import { noop } from '../../../shared/utils.js'; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2c9e4db911aa..bfbb95a8db7c 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -553,15 +553,16 @@ export function unlink_effect(effect) { * A paused effect does not update, and the DOM subtree becomes inert. * @param {Effect} effect * @param {() => void} [callback] + * @param {boolean} [destroy] */ -export function pause_effect(effect, callback) { +export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; pause_children(effect, transitions, true); run_out_transitions(transitions, () => { - destroy_effect(effect); + if (destroy) destroy_effect(effect); if (callback) callback(); }); } @@ -662,3 +663,20 @@ function resume_children(effect, local) { export function aborted(effect = /** @type {Effect} */ (active_effect)) { return (effect.f & DESTROYED) !== 0; } + +/** + * @param {Effect} effect + * @param {DocumentFragment} fragment + */ +export function move_effect(effect, fragment) { + var node = effect.nodes_start; + var end = effect.nodes_end; + + while (node !== null) { + /** @type {TemplateNode | null} */ + var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + fragment.append(node); + node = next; + } +} From a168ecdd5bee5925790d6c3003a5143d72f6d13f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 11:35:02 -0400 Subject: [PATCH 03/26] WIP --- .../internal/client/dom/blocks/branches.js | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 9adb3ddec93a..6b88f39a8053 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -96,29 +96,27 @@ export class BranchManager { * @param {(target: TemplateNode) => void} fn */ ensure(key, fn) { - if (should_defer_append()) { - var batch = /** @type {Batch} */ (current_batch); + var batch = /** @type {Batch} */ (current_batch); - if (!this.#onscreen.has(key) && !this.#offscreen.has(key)) { - var fragment = document.createDocumentFragment(); - var target = create_text(); + if (!this.#onscreen.has(key) && !this.#offscreen.has(key)) { + var fragment = document.createDocumentFragment(); + var target = create_text(); - fragment.append(target); + fragment.append(target); - this.#offscreen.set(key, { - effect: branch(() => fn(target)), - fragment - }); - } + this.#offscreen.set(key, { + effect: branch(() => fn(target)), + fragment + }); + } - this.#batches.set(batch, key); + this.#batches.set(batch, key); + // TODO in the no-defer case, we could skip the offscreen step + if (should_defer_append()) { batch.add_callback(this.#commit); } else { - this.#onscreen.set( - key, - branch(() => fn(this.#anchor)) - ); + this.#commit(); } } } From 65f597029ba4b9c8012498fb656a2d4a4a3f655e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 11:48:36 -0400 Subject: [PATCH 04/26] WIP --- .../src/internal/client/dom/blocks/branches.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 6b88f39a8053..15d3f0985b11 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -114,6 +114,22 @@ export class BranchManager { // TODO in the no-defer case, we could skip the offscreen step if (should_defer_append()) { + 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 { this.#commit(); From f3fc4438acbcaddefcae28162a3dddfc2727a441 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 14:19:29 -0400 Subject: [PATCH 05/26] WIP --- .../phases/3-transform/client/transform-client.js | 4 +++- .../3-transform/client/visitors/Fragment.js | 6 +++++- .../src/internal/client/reactivity/async.js | 15 ++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 2629379f63d9..2431ae9f3e68 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -384,7 +384,9 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + component_block.body.push( + b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true))) + ); } else { component_block.body.push( ...state.instance_level_snippets, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 85d8e3caffb2..e84ac357e3fb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -177,7 +177,11 @@ export function Fragment(node, context) { } if (has_await) { - return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]); + return b.block([ + b.stmt( + b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true)) + ) + ]); } else { return b.block(body); } diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 45c78ff926b9..f284ec1831ed 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,5 +1,4 @@ -/** @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'; @@ -28,6 +27,7 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; +import { create_text } from '../dom/operations.js'; /** * @@ -197,9 +197,10 @@ export function unset_context() { } /** - * @param {() => Promise} fn + * @param {TemplateNode} anchor + * @param {(target: TemplateNode) => Promise} fn */ -export async function async_body(fn) { +export async function async_body(anchor, fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var pending = boundary.is_pending(); @@ -217,8 +218,11 @@ export async function async_body(fn) { next_hydrate_node = skip_nodes(false); } + var target = create_text(); + anchor.before(target); + try { - var promise = fn(); + var promise = fn(target); } finally { if (next_hydrate_node) { set_hydrate_node(next_hydrate_node); @@ -228,6 +232,7 @@ export async function async_body(fn) { try { await promise; + target.remove(); } catch (error) { if (!aborted(active)) { invoke_error_boundary(error, active); From cc15ffb362ee7445e470477cb786c7787999fc78 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 14:43:06 -0400 Subject: [PATCH 06/26] fix hydration --- .../internal/client/dom/blocks/branches.js | 11 ++++++--- .../src/internal/client/dom/blocks/if.js | 24 +++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 15d3f0985b11..914f62a9d5aa 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -7,6 +7,7 @@ import { pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; /** @@ -18,7 +19,7 @@ import { create_text, should_defer_append } from '../operations.js'; */ export class BranchManager { /** @type {TemplateNode} */ - #anchor; + anchor; /** @type {Map} */ #batches = new Map(); @@ -34,7 +35,7 @@ export class BranchManager { * @param {TemplateNode} anchor */ constructor(anchor) { - this.#anchor = anchor; + this.anchor = anchor; } #commit = () => { @@ -58,7 +59,7 @@ export class BranchManager { /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove(); // ...and append the fragment - this.#anchor.before(offscreen.fragment); + this.anchor.before(offscreen.fragment); } this.#batches.delete(batch); @@ -132,6 +133,10 @@ export class BranchManager { batch.add_callback(this.#commit); } else { + if (hydrating) { + this.anchor = hydrate_node; + } + this.#commit(); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 23a4555e5bac..0be665fb07e9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -29,9 +29,6 @@ export function if_block(node, fn, elseif = false) { var anchor = node; - /** @type {typeof UNINITIALIZED | boolean | null} */ - var condition = UNINITIALIZED; - var flags = elseif ? EFFECT_TRANSPARENT : 0; var has_branch = false; @@ -44,12 +41,9 @@ export function if_block(node, fn, elseif = false) { var branches = new BranchManager(anchor); const update_branch = ( - /** @type {boolean | null} */ new_condition, + /** @type {boolean} */ condition, /** @type {null | ((anchor: Node) => void)} */ fn ) => { - /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - let mismatch = false; - if (hydrating) { const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE; @@ -59,24 +53,24 @@ export function if_block(node, fn, elseif = false) { anchor = skip_nodes(); set_hydrate_node(anchor); + branches.anchor = anchor; + set_hydrating(false); - mismatch = true; + branches.ensure(condition, fn ?? noop); + set_hydrating(true); + + return; } } - branches.ensure(new_condition, fn ?? noop); - - if (mismatch) { - // continue in hydration mode - set_hydrating(true); - } + branches.ensure(condition, fn ?? noop); }; block(() => { has_branch = false; fn(set_branch); if (!has_branch) { - update_branch(null, null); + update_branch(false, null); } }, flags); From 5b17fdd2d05247b78bae951d06773c327ead409d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 14:50:48 -0400 Subject: [PATCH 07/26] simplify --- .../src/internal/client/dom/blocks/if.js | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 0be665fb07e9..8190a4325eb6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -2,7 +2,6 @@ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, - hydrate_node, hydrating, read_hydration_instruction, skip_nodes, @@ -10,7 +9,7 @@ import { set_hydrating } from '../hydration.js'; import { block } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; +import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { BranchManager } from './branches.js'; import { noop } from '../../../shared/utils.js'; @@ -27,54 +26,46 @@ export function if_block(node, fn, elseif = false) { hydrate_next(); } - var anchor = node; - + 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); - }; - - var branches = new BranchManager(anchor); - - const update_branch = ( - /** @type {boolean} */ condition, - /** @type {null | ((anchor: Node) => void)} */ fn - ) => { + /** + * @param {boolean} condition, + * @param {((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); branches.anchor = anchor; set_hydrating(false); - branches.ensure(condition, fn ?? noop); + branches.ensure(condition, fn); set_hydrating(true); return; } } - branches.ensure(condition, fn ?? noop); - }; + branches.ensure(condition, fn); + } block(() => { - has_branch = false; - fn(set_branch); + var has_branch = false; + + fn((fn, flag = true) => { + has_branch = true; + update_branch(flag, fn); + }); + if (!has_branch) { - update_branch(false, null); + update_branch(false, noop); } }, flags); - - if (hydrating) { - anchor = hydrate_node; - } } From ff48b2329c77e329128dcc151c634fcdce973a9a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 15:19:52 -0400 Subject: [PATCH 08/26] all tests passing --- .../src/internal/client/dom/blocks/branches.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 914f62a9d5aa..b977ba4e667c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -40,6 +40,10 @@ export class BranchManager { #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); @@ -64,6 +68,20 @@ export class BranchManager { this.#batches.delete(batch); + for (const [b, k] of this.#batches) { + if (b === batch) break; + + const offscreen = this.#offscreen.get(k); + + if (offscreen) { + destroy_effect(offscreen.effect); + this.#offscreen.delete(k); + } + + this.#batches.delete(b); + } + + // outro/destroy effects for (const [k, effect] of this.#onscreen) { if (k === key) continue; From d168bb137ea82e129bf553a8752f062f56adfce6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 15:34:44 -0400 Subject: [PATCH 09/26] key blocks --- .../internal/client/dom/blocks/branches.js | 8 +++ .../src/internal/client/dom/blocks/key.js | 68 ++----------------- 2 files changed, 14 insertions(+), 62 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index b977ba4e667c..16ca4b1d6827 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -1,4 +1,5 @@ /** @import { Effect, TemplateNode } from '#client' */ +import { is_runes } from '../../context.js'; import { Batch, current_batch } from '../../reactivity/batch.js'; import { branch, @@ -30,6 +31,8 @@ export class BranchManager { /** @type {Map} */ #offscreen = new Map(); + #legacy = !is_runes(); + /** * * @param {TemplateNode} anchor @@ -117,6 +120,11 @@ export class BranchManager { ensure(key, fn) { var batch = /** @type {Batch} */ (current_batch); + // key blocks in Svelte <5 had stupid semantics + if (this.#legacy && typeof key === 'object') { + key = {}; + } + if (!this.#onscreen.has(key) && !this.#offscreen.has(key)) { var fragment = document.createDocumentFragment(); var target = create_text(); diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 5e3c42019f33..143b225d7f4d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,12 +1,7 @@ -/** @import { Effect, TemplateNode } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ -import { UNINITIALIZED } from '../../../../constants.js'; -import { block, branch, pause_effect } from '../../reactivity/effects.js'; -import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; -import { is_runes } from '../../context.js'; -import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { create_text, should_defer_append } from '../operations.js'; -import { current_batch } from '../../reactivity/batch.js'; +/** @import { TemplateNode } from '#client' */ +import { block } from '../../reactivity/effects.js'; +import { hydrate_next, hydrating } from '../hydration.js'; +import { BranchManager } from './branches.js'; /** * @template V @@ -20,60 +15,9 @@ export function key(node, get_key, render_fn) { hydrate_next(); } - var anchor = 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 branches = new BranchManager(node); 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)); - - if (defer) { - /** @type {Batch} */ (current_batch).add_callback(commit); - } else { - commit(); - } - } + branches.ensure(get_key(), render_fn); }); - - if (hydrating) { - anchor = hydrate_node; - } } From e3b15104ec9008b78a8afe19d19de055eeef5631 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 15:41:01 -0400 Subject: [PATCH 10/26] snippets --- .../src/internal/client/dom/blocks/snippet.js | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 32d88d4c606a..6c0fd71789f1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,8 +1,8 @@ /** @import { Snippet } from 'svelte' */ -/** @import { Effect, TemplateNode } from '#client' */ +/** @import { TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; -import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; +import { block, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, set_dev_current_component_function @@ -14,8 +14,8 @@ import * as w from '../../warnings.js'; import * as e from '../../errors.js'; import { DEV } from 'esm-env'; import { get_first_child, get_next_sibling } from '../operations.js'; -import { noop } from '../../../shared/utils.js'; import { prevent_snippet_stringification } from '../../../shared/validate.js'; +import { BranchManager } from './branches.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -25,33 +25,17 @@ import { prevent_snippet_stringification } from '../../../shared/validate.js'; * @returns {void} */ export function snippet(node, get_snippet, ...args) { - var anchor = node; - - /** @type {SnippetFn | null | undefined} */ - // @ts-ignore - var snippet = noop; - - /** @type {Effect | null} */ - var snippet_effect; + var branches = new BranchManager(node); block(() => { - if (snippet === (snippet = get_snippet())) return; + const snippet = get_snippet(); - if (snippet_effect) { - destroy_effect(snippet_effect); - snippet_effect = null; - } - - if (DEV && snippet == null) { + if (snippet == null) { e.invalid_snippet(); } - snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); + branches.ensure(snippet, (anchor) => snippet(anchor, ...args)); }, EFFECT_TRANSPARENT); - - if (hydrating) { - anchor = hydrate_node; - } } /** From 3c2eb4c15df4823952a6d48cf053b89a4d6e89c3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 16:16:07 -0400 Subject: [PATCH 11/26] fix --- .../internal/client/dom/blocks/branches.js | 19 +++--- .../src/internal/client/dom/blocks/if.js | 4 +- .../src/internal/client/dom/blocks/snippet.js | 6 +- .../client/dom/blocks/svelte-component.js | 62 ++----------------- 4 files changed, 20 insertions(+), 71 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 16ca4b1d6827..358b87ee41c7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -57,16 +57,17 @@ export class BranchManager { } else { // effect is currently offscreen. put it in the DOM var offscreen = this.#offscreen.get(key); - if (!offscreen) throw new Error('This should never happen!'); - this.#onscreen.set(key, offscreen.effect); - this.#offscreen.delete(key); + if (offscreen) { + this.#onscreen.set(key, offscreen.effect); + this.#offscreen.delete(key); - // remove the anchor... - /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove(); + // remove the anchor... + /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove(); - // ...and append the fragment - this.anchor.before(offscreen.fragment); + // ...and append the fragment + this.anchor.before(offscreen.fragment); + } } this.#batches.delete(batch); @@ -115,7 +116,7 @@ export class BranchManager { /** * * @param {any} key - * @param {(target: TemplateNode) => void} fn + * @param {null | ((target: TemplateNode) => void)} fn */ ensure(key, fn) { var batch = /** @type {Batch} */ (current_batch); @@ -125,7 +126,7 @@ export class BranchManager { key = {}; } - if (!this.#onscreen.has(key) && !this.#offscreen.has(key)) { + if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) { var fragment = document.createDocumentFragment(); var target = create_text(); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 8190a4325eb6..1d6b2d9163a6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -31,7 +31,7 @@ export function if_block(node, fn, elseif = false) { /** * @param {boolean} condition, - * @param {((anchor: Node) => void)} fn + * @param {null | ((anchor: Node) => void)} fn */ function update_branch(condition, fn) { if (hydrating) { @@ -65,7 +65,7 @@ export function if_block(node, fn, elseif = false) { }); if (!has_branch) { - update_branch(false, noop); + update_branch(false, null); } }, flags); } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 6c0fd71789f1..0c4948aca027 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -28,13 +28,13 @@ export function snippet(node, get_snippet, ...args) { var branches = new BranchManager(node); block(() => { - const snippet = get_snippet(); + const snippet = get_snippet() ?? null; - if (snippet == null) { + if (DEV && snippet == null) { e.invalid_snippet(); } - branches.ensure(snippet, (anchor) => snippet(anchor, ...args)); + branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args))); }, EFFECT_TRANSPARENT); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 2697722b3953..53821a95ca39 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -5,6 +5,8 @@ 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 { BranchManager } from './branches.js'; +import { noop } from '../../../shared/utils.js'; /** * @template P @@ -19,64 +21,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; - } } From 5f01369c68d7c68d6225ee997387cedc6a15aa07 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 16:17:29 -0400 Subject: [PATCH 12/26] tidy up --- packages/svelte/src/internal/client/dom/blocks/if.js | 1 - .../src/internal/client/dom/blocks/svelte-component.js | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 1d6b2d9163a6..7fa5ca464dd1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -11,7 +11,6 @@ import { import { block } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { BranchManager } from './branches.js'; -import { noop } from '../../../shared/utils.js'; // TODO reinstate https://github.com/sveltejs/svelte/pull/15250 diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 53821a95ca39..134e57e62710 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,12 +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'; -import { noop } from '../../../shared/utils.js'; /** * @template P From 3a1820d102d4146651aefe82fb7eb4ce51e27ef8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 17:38:28 -0400 Subject: [PATCH 13/26] WIP await --- .../src/internal/client/dom/blocks/await.js | 173 ++++++------------ .../src/internal/client/reactivity/async.js | 2 +- 2 files changed, 59 insertions(+), 116 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index e7917fbd9e45..d71c8dcc3d49 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -23,6 +23,8 @@ import { set_dev_stack } 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 +35,7 @@ const CATCH = 2; /** * @template V * @param {TemplateNode} node - * @param {(() => Promise)} get_input + * @param {(() => any)} get_input * @param {null | ((anchor: Node) => void)} pending_fn * @param {null | ((anchor: Node, value: Source) => void)} then_fn * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn @@ -44,149 +46,95 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { hydrate_next(); } - var anchor = node; var runes = is_runes(); - var active_component_context = component_context; - - /** @type {any} */ - var component_function = DEV ? component_context?.function : null; - var dev_original_stack = DEV ? dev_stack : null; - - /** @type {V | Promise | typeof UNINITIALIZED} */ - var input = UNINITIALIZED; - - /** @type {Effect | null} */ - var pending_effect; - - /** @type {Effect | null} */ - var then_effect; - - /** @type {Effect | null} */ - var catch_effect; var input_source = runes ? source(/** @type {V} */ (undefined)) : mutable_source(/** @type {V} */ (undefined), false, false); var error_source = runes ? source(undefined) : mutable_source(undefined, false, false); - var resolved = false; - /** - * @param {AwaitState} state - * @param {boolean} restore - */ - function update(state, restore) { - resolved = true; - - if (restore) { - set_active_effect(effect); - set_active_reaction(effect); // TODO do we need both? - set_component_context(active_component_context); - if (DEV) { - set_dev_current_component_function(component_function); - set_dev_stack(dev_original_stack); - } - } - try { - if (state === PENDING && pending_fn) { - if (pending_effect) resume_effect(pending_effect); - else pending_effect = branch(() => pending_fn(anchor)); - } - - if (state === THEN && then_fn) { - if (then_effect) resume_effect(then_effect); - else then_effect = branch(() => then_fn(anchor, input_source)); - } - - if (state === CATCH && catch_fn) { - if (catch_effect) resume_effect(catch_effect); - else catch_effect = branch(() => catch_fn(anchor, error_source)); - } - - if (state !== PENDING && pending_effect) { - pause_effect(pending_effect, () => (pending_effect = null)); - } - - if (state !== THEN && then_effect) { - pause_effect(then_effect, () => (then_effect = null)); - } - - if (state !== CATCH && catch_effect) { - pause_effect(catch_effect, () => (catch_effect = null)); - } - } finally { - if (restore) { - if (DEV) { - set_dev_current_component_function(null); - set_dev_stack(null); - } + var branches = new BranchManager(node); - set_component_context(null); - set_active_reaction(null); - set_active_effect(null); - - // 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 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); + 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) { + // we want to restore everything _except_ this + set_hydrating(false); + } + + try { + fn(); + } finally { + unset_context(); - resolved = 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(); + } + }; - promise.then( + input.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); + resolve(() => { + internal_set(input_source, value); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, input_source))); + }); }, (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; - } + resolve(() => { + internal_set(error_source, error); + branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error_source))); + + if (!catch_fn) { + // Rethrow the error if no catch block exists + throw error_source.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); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, input_source))); } if (mismatch) { @@ -194,11 +142,6 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { set_hydrating(true); } - // Set the input to something else, in order to disable the promise callbacks - return () => (input = UNINITIALIZED); + return () => (destroyed = true); }); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index f284ec1831ed..45369900b21a 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -80,7 +80,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; From 82fe9449ac273e4ee569a717d6bcc82a3212e84e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 19:21:26 -0400 Subject: [PATCH 14/26] tidy up --- .../src/internal/client/dom/blocks/await.js | 22 ++++++------------- .../samples/await-pending-destroy/_config.js | 3 ++- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index d71c8dcc3d49..dd617168fb61 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,27 +1,17 @@ -/** @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, set_hydrating } 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 { HYDRATION_START_ELSE } from '../../../../constants.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'; @@ -142,6 +132,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { set_hydrating(true); } - return () => (destroyed = true); + return () => { + destroyed = true; + }; }); } diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js index 1725cd8f6fb0..9ef598de6c83 100644 --- a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js @@ -1,3 +1,4 @@ +import { tick } from 'svelte'; import { test } from '../../test'; /** @@ -77,7 +78,7 @@ export default test({ const { promise, reject } = promiseWithResolver(); component.promise = promise; // wait for rendering - await Promise.resolve(); + await tick(); // remove the promise component.promise = null; From fe1cc8f2e31af4c3ad7d42f691e75ac6644fea69 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 19:34:45 -0400 Subject: [PATCH 15/26] fix --- .../src/internal/client/reactivity/async.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 45369900b21a..76ade0950462 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,7 +1,13 @@ /** @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 { @@ -92,6 +98,10 @@ export 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 @@ export function capture() { if (DEV) { set_from_async_derived(null); + set_dev_stack(previous_dev_stack); } }; } @@ -193,7 +204,11 @@ 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); + } } /** From cb7203a572b9c5e9a9236395e4f43756121a763e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 19:45:11 -0400 Subject: [PATCH 16/26] neaten up --- .../src/internal/client/dom/blocks/await.js | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index dd617168fb61..cab6bdc147b5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -10,7 +10,7 @@ import { set_hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; -import { HYDRATION_START_ELSE } from '../../../../constants.js'; +import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { is_runes } from '../../context.js'; import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; @@ -38,10 +38,9 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { var runes = is_runes(); - 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 v = /** @type {V} */ (UNINITIALIZED); + var value = runes ? source(v) : mutable_source(v, false, false); + var error = runes ? source(v) : mutable_source(v, false, false); var branches = new BranchManager(node); @@ -90,20 +89,20 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { }; input.then( - (value) => { + (v) => { resolve(() => { - internal_set(input_source, value); - branches.ensure(THEN, then_fn && ((target) => then_fn(target, input_source))); + internal_set(value, v); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); }); }, - (error) => { + (e) => { resolve(() => { - internal_set(error_source, error); - branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error_source))); + 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_source.v; + throw error.v; } }); } @@ -123,8 +122,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { }); } } else { - internal_set(input_source, input); - branches.ensure(THEN, then_fn && ((target) => then_fn(target, input_source))); + internal_set(value, input); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); } if (mismatch) { From 82a63f278e3d970939f74fec02f7b7c7af3852ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 19:46:21 -0400 Subject: [PATCH 17/26] unused --- packages/svelte/src/internal/client/dom/blocks/await.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index cab6bdc147b5..53d3ba67f9ec 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -56,7 +56,6 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { // Hydration mismatch: remove everything inside the anchor and start fresh set_hydrate_node(skip_nodes()); set_hydrating(false); - mismatch = true; } if (is_promise(input)) { From 446ffc252bea2eee85ee76ffbd8a485e7f5a66d3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 19:47:31 -0400 Subject: [PATCH 18/26] tweak --- packages/svelte/src/internal/client/dom/blocks/await.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 53d3ba67f9ec..7fe04934bc60 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -49,7 +49,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { 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 + // @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) { From a38230421977648b1b6f911ad051587d5e807ac0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 20:54:37 -0400 Subject: [PATCH 19/26] elements --- .../internal/client/dom/blocks/branches.js | 80 ++++++++++++------- .../client/dom/blocks/svelte-element.js | 79 ++++++++---------- packages/svelte/src/internal/client/render.js | 1 + 3 files changed, 84 insertions(+), 76 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 358b87ee41c7..338c70a13ec8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -8,6 +8,7 @@ import { 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'; @@ -34,11 +35,18 @@ export class BranchManager { #legacy = !is_runes(); /** - * + * Whether to pause (i.e. outro) on change, or destroy immediately. + * This is necessary for `` + */ + #transition = true; + + /** * @param {TemplateNode} anchor + * @param {boolean} transition */ - constructor(anchor) { + constructor(anchor, transition = true) { this.anchor = anchor; + this.#transition = transition; } #commit = () => { @@ -67,6 +75,7 @@ export class BranchManager { // ...and append the fragment this.anchor.before(offscreen.fragment); + onscreen = offscreen.effect; } } @@ -89,27 +98,29 @@ export class BranchManager { for (const [k, effect] of this.#onscreen) { if (k === key) continue; - pause_effect( - effect, - () => { - const keys = Array.from(this.#batches.values()); + 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); + 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? + fragment.append(create_text()); // TODO can we avoid this? - this.#offscreen.set(k, { effect, fragment }); - } else { - destroy_effect(effect); - } + this.#offscreen.set(k, { effect, fragment }); + } else { + destroy_effect(effect); + } - this.#onscreen.delete(k); - }, - false - ); + this.#onscreen.delete(k); + }; + + if (this.#transition || !onscreen) { + pause_effect(effect, on_destroy, false); + } else { + on_destroy(); + } } }; @@ -120,28 +131,35 @@ export class BranchManager { */ ensure(key, fn) { var batch = /** @type {Batch} */ (current_batch); + var defer = should_defer_append(); // key blocks in Svelte <5 had stupid semantics - if (this.#legacy && typeof key === 'object') { + if (this.#legacy && key !== null && typeof key === 'object') { key = {}; } if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) { - var fragment = document.createDocumentFragment(); - var target = create_text(); - - fragment.append(target); - - this.#offscreen.set(key, { - effect: branch(() => fn(target)), - fragment - }); + 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); - // TODO in the no-defer case, we could skip the offscreen step - if (should_defer_append()) { + if (defer) { for (const [k, effect] of this.#onscreen) { if (k === key) { batch.skipped_effects.delete(effect); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 231a3621b1af..6533ff8921a2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -8,13 +8,7 @@ import { set_hydrating } from '../hydration.js'; import { create_text, get_first_child } from '../operations.js'; -import { - block, - branch, - destroy_effect, - pause_effect, - resume_effect -} from '../../reactivity/effects.js'; +import { block, teardown } from '../../reactivity/effects.js'; import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; @@ -23,6 +17,7 @@ import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; import { is_raw_text_element } from '../../../../utils.js'; +import { BranchManager } from './branches.js'; /** * @param {Comment | Element} node @@ -42,12 +37,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var filename = DEV && location && component_context?.function[FILENAME]; - /** @type {string | null} */ - var tag; - - /** @type {string | null} */ - var current_tag; - /** @type {null | Element} */ var element = null; @@ -58,9 +47,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node); - /** @type {Effect | null} */ - var effect; - /** * The keyed `{#each ...}` item block, if any, that this element is inside. * We track this so we can set it when changing the element, allowing any @@ -68,36 +54,24 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio */ var each_item_block = current_each_item; + var branches = new BranchManager(anchor, false); + block(() => { const next_tag = get_tag() || null; var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null; - // Assumption: Noone changes the namespace but not the tag (what would that even mean?) - if (next_tag === tag) return; - - // See explanation of `each_item_block` above - var previous_each_item = current_each_item; - set_current_each_item(each_item_block); - - if (effect) { - if (next_tag === null) { - // start outro - pause_effect(effect, () => { - effect = null; - current_tag = null; - }); - } else if (next_tag === current_tag) { - // same tag as is currently rendered — abort outro - resume_effect(effect); - } else { - // tag is changing — destroy immediately, render contents without intro transitions - destroy_effect(effect); - set_should_intro(false); - } + if (next_tag === null) { + branches.ensure(null, null); + set_should_intro(true); + return; } - if (next_tag && next_tag !== current_tag) { - effect = branch(() => { + branches.ensure(next_tag, (anchor) => { + // See explanation of `each_item_block` above + var previous_each_item = current_each_item; + set_current_each_item(each_item_block); + + if (next_tag) { element = hydrating ? /** @type {Element} */ (element) : ns @@ -149,16 +123,31 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio /** @type {Effect} */ (active_effect).nodes_end = element; anchor.before(element); - }); - } + } + + set_current_each_item(previous_each_item); + + if (hydrating) { + set_hydrate_node(anchor); + } + }); - tag = next_tag; - if (tag) current_tag = tag; + // revert to the default state after the effect has been created set_should_intro(true); - set_current_each_item(previous_each_item); + return () => { + if (next_tag) { + // if we're in this callback because we're re-running the effect, + // disable intros (unless no element is currently displayed) + set_should_intro(false); + } + }; }, EFFECT_TRANSPARENT); + teardown(() => { + set_should_intro(true); + }); + if (was_hydrating) { set_hydrating(true); set_hydrate_node(anchor); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index b1165a6e7aee..9412036becb3 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -137,6 +137,7 @@ export function hydrate(component, options) { if (error !== HYDRATION_ERROR) { // eslint-disable-next-line no-console console.warn('Failed to hydrate: ', error); + console.error(error); } if (options.recover === false) { From 7c0c71b72d272a2b2d5a1891262e967dbadd8c68 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 21:08:28 -0400 Subject: [PATCH 20/26] changeset --- .changeset/yellow-shrimps-provide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/yellow-shrimps-provide.md diff --git a/.changeset/yellow-shrimps-provide.md b/.changeset/yellow-shrimps-provide.md new file mode 100644 index 000000000000..a29385660ad4 --- /dev/null +++ b/.changeset/yellow-shrimps-provide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: centralise branch management From b9208dbe90fc5f2ac1887131f4ded42f5af141cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 21:18:36 -0400 Subject: [PATCH 21/26] fix --- packages/svelte/src/internal/client/render.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 9412036becb3..b1165a6e7aee 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -137,7 +137,6 @@ export function hydrate(component, options) { if (error !== HYDRATION_ERROR) { // eslint-disable-next-line no-console console.warn('Failed to hydrate: ', error); - console.error(error); } if (options.recover === false) { From d5ba3e24ba5a49c380b1a42f571bc03d51c2381a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 19 Oct 2025 09:32:18 -0400 Subject: [PATCH 22/26] preserve newer batches --- .../src/internal/client/dom/blocks/branches.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 338c70a13ec8..9f7cd9dca923 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -79,19 +79,22 @@ export class BranchManager { } } - this.#batches.delete(batch); - for (const [b, k] of this.#batches) { - if (b === batch) break; + 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); } - - this.#batches.delete(b); } // outro/destroy effects From d331f66321a50f1dd62b8fc26ed56a5a0640a9a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 19 Oct 2025 09:33:19 -0400 Subject: [PATCH 23/26] add comment --- packages/svelte/src/internal/client/dom/blocks/branches.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 9f7cd9dca923..0b4a9f2d6188 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -97,8 +97,9 @@ export class BranchManager { } } - // outro/destroy effects + // 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 = () => { From ff300d570f771e63b94c2431e6f9a28ebee2d0f0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 20 Oct 2025 07:42:15 -0400 Subject: [PATCH 24/26] add comment --- packages/svelte/src/internal/client/dom/blocks/await.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 7fe04934bc60..bac01e4c3393 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -72,7 +72,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { restore(); if (hydrating) { - // we want to restore everything _except_ this + // `restore()` could set `hydrating` to `true`, which we very much + // don't want — we want to restore everything _except_ this set_hydrating(false); } From 6aeaa38950ced8458282d9a0299f13373fa9ff21 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 20 Oct 2025 07:42:24 -0400 Subject: [PATCH 25/26] no longer necessary apparently? --- packages/svelte/src/internal/client/reactivity/async.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 76ade0950462..1d408744fdfc 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -233,11 +233,8 @@ export async function async_body(anchor, fn) { next_hydrate_node = skip_nodes(false); } - var target = create_text(); - anchor.before(target); - try { - var promise = fn(target); + var promise = fn(anchor); } finally { if (next_hydrate_node) { set_hydrate_node(next_hydrate_node); @@ -247,7 +244,6 @@ export async function async_body(anchor, fn) { try { await promise; - target.remove(); } catch (error) { if (!aborted(active)) { invoke_error_boundary(error, active); From 4efcc529e21c92f97746c599dec301770627f78f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 20 Oct 2025 07:48:21 -0400 Subject: [PATCH 26/26] move legacy logic to key block --- .../src/internal/client/dom/blocks/branches.js | 7 ------- .../svelte/src/internal/client/dom/blocks/key.js | 12 +++++++++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 0b4a9f2d6188..827f9f44faf4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -32,8 +32,6 @@ export class BranchManager { /** @type {Map} */ #offscreen = new Map(); - #legacy = !is_runes(); - /** * Whether to pause (i.e. outro) on change, or destroy immediately. * This is necessary for `` @@ -137,11 +135,6 @@ export class BranchManager { var batch = /** @type {Batch} */ (current_batch); var defer = should_defer_append(); - // key blocks in Svelte <5 had stupid semantics - if (this.#legacy && key !== null && typeof key === 'object') { - key = {}; - } - if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) { if (defer) { var fragment = document.createDocumentFragment(); diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 143b225d7f4d..849b1c24472c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,4 +1,5 @@ /** @import { TemplateNode } from '#client' */ +import { is_runes } from '../../context.js'; import { block } from '../../reactivity/effects.js'; import { hydrate_next, hydrating } from '../hydration.js'; import { BranchManager } from './branches.js'; @@ -17,7 +18,16 @@ export function key(node, get_key, render_fn) { var branches = new BranchManager(node); + var legacy = !is_runes(); + block(() => { - branches.ensure(get_key(), render_fn); + var key = get_key(); + + // key blocks in Svelte <5 had stupid semantics + if (legacy && key !== null && typeof key === 'object') { + key = /** @type {V} */ ({}); + } + + branches.ensure(key, render_fn); }); }