From 9b97b7fbf7005a3075715f423fec4c28b37cd0be Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Sat, 5 Sep 2026 10:15:27 +0200 Subject: [PATCH] fix: try finally pop render --- .../internal/client/dom/blocks/boundary.js | 58 ++++---- .../internal/client/dom/blocks/branches.js | 134 +++++++++--------- .../src/internal/client/dom/blocks/each.js | 42 +++--- .../src/internal/client/reactivity/effects.js | 22 +-- 4 files changed, 132 insertions(+), 124 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a9665f6c1a..738e89de9e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -277,45 +277,47 @@ export class Boundary { queue_micro_task(() => { var pop_renderer = push_renderer(this.#effect.r); - var fragment = (this.#offscreen_fragment = create_fragment()); - var anchor = create_text(); - var handled = false; + try { + var fragment = (this.#offscreen_fragment = create_fragment()); + var anchor = create_text(); + var handled = false; - append_child(fragment, anchor); + append_child(fragment, anchor); - this.#main_effect = this.#run(() => { - try { - return branch(() => this.#children(anchor)); - } catch (error) { + this.#main_effect = this.#run(() => { try { - this.error(error); - handled = true; + return branch(() => this.#children(anchor)); } catch (error) { - invoke_error_boundary(error, this.#effect.parent); + try { + this.error(error); + handled = true; + } catch (error) { + invoke_error_boundary(error, this.#effect.parent); + } + + return null; } + }); - return null; + if (this.#main_effect === null) { + this.#offscreen_fragment = null; + if (handled) this.#resolve(/** @type {Batch} */ (current_batch)); + return; } - }); - - if (this.#main_effect === null) { - this.#offscreen_fragment = null; - if (handled) this.#resolve(/** @type {Batch} */ (current_batch)); - return; - } - if (this.#pending_count === 0) { - insert_before(this.#anchor, fragment); - this.#offscreen_fragment = null; + if (this.#pending_count === 0) { + insert_before(this.#anchor, fragment); + this.#offscreen_fragment = null; - pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { - this.#pending_effect = null; - }); + pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { + this.#pending_effect = null; + }); - this.#resolve(/** @type {Batch} */ (current_batch)); + this.#resolve(/** @type {Batch} */ (current_batch)); + } + } finally { + pop_renderer?.(); } - - pop_renderer?.(); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 2c045df256..7129d1f3bc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -98,91 +98,93 @@ export class BranchManager { var pop_renderer = push_renderer(this.#renderer); - var key = /** @type {Key} */ (this.#batches.get(batch)); + try { + var key = /** @type {Key} */ (this.#batches.get(batch)); - var onscreen = this.#onscreen.get(key); + var onscreen = this.#onscreen.get(key); - if (onscreen) { - // effect is already in the DOM — abort any current outro - resume_effect(onscreen); - this.#outroing.delete(key); - } else { - // effect is currently offscreen. put it in the DOM - var offscreen = this.#offscreen.get(key); - - if (offscreen) { - // effect could have been outro'ed before through a prior batch — resume if necessary - resume_effect(offscreen.effect); - this.#onscreen.set(key, offscreen.effect); - this.#offscreen.delete(key); - - if (DEV) { - // Tell hmr.js about the anchor it should use for updates, - // since the initial one will be removed - /** @type {any} */ (get_last_child(offscreen.fragment))[HMR_ANCHOR] = this.anchor; + if (onscreen) { + // effect is already in the DOM — abort any current outro + resume_effect(onscreen); + this.#outroing.delete(key); + } else { + // effect is currently offscreen. put it in the DOM + var offscreen = this.#offscreen.get(key); + + if (offscreen) { + // effect could have been outro'ed before through a prior batch — resume if necessary + resume_effect(offscreen.effect); + this.#onscreen.set(key, offscreen.effect); + this.#offscreen.delete(key); + + if (DEV) { + // Tell hmr.js about the anchor it should use for updates, + // since the initial one will be removed + /** @type {any} */ (get_last_child(offscreen.fragment))[HMR_ANCHOR] = this.anchor; + } + + // remove the anchor... + remove_node(/** @type {ChildNode} */ (get_last_child(offscreen.fragment))); + + // ...and append the fragment + insert_before(this.anchor, offscreen.fragment); + onscreen = offscreen.effect; } + } - // remove the anchor... - remove_node(/** @type {ChildNode} */ (get_last_child(offscreen.fragment))); + for (const [b, k] of this.#batches) { + this.#batches.delete(b); - // ...and append the fragment - insert_before(this.anchor, offscreen.fragment); - onscreen = offscreen.effect; - } - } + if (b === batch) { + // keep values for newer batches + break; + } - for (const [b, k] of this.#batches) { - this.#batches.delete(b); + const offscreen = this.#offscreen.get(k); - if (b === batch) { - // keep values for newer batches - break; + if (offscreen) { + // for older batches, destroy offscreen effects + // as they will never be committed + destroy_effect(offscreen.effect); + this.#offscreen.delete(k); + } } - const offscreen = this.#offscreen.get(k); + // outro/destroy all onscreen effects... + for (const [k, effect] of this.#onscreen) { + // ...except the one that was just committed + // or those that are already outroing (else the transition is aborted and the effect destroyed right away) + if (k === key || this.#outroing.has(k)) continue; - if (offscreen) { - // for older batches, destroy offscreen effects - // as they will never be committed - destroy_effect(offscreen.effect); - this.#offscreen.delete(k); - } - } + const on_destroy = () => { + const keys = Array.from(this.#batches.values()); - // outro/destroy all onscreen effects... - for (const [k, effect] of this.#onscreen) { - // ...except the one that was just committed - // or those that are already outroing (else the transition is aborted and the effect destroyed right away) - if (k === key || this.#outroing.has(k)) continue; + if (keys.includes(k)) { + // keep the effect offscreen, as another batch will need it + var fragment = create_fragment(); + move_effect(effect, fragment); - const on_destroy = () => { - const keys = Array.from(this.#batches.values()); + append_child(fragment, create_text()); // TODO can we avoid this? - if (keys.includes(k)) { - // keep the effect offscreen, as another batch will need it - var fragment = create_fragment(); - move_effect(effect, fragment); + this.#offscreen.set(k, { effect, fragment }); + } else { + destroy_effect(effect); + } - append_child(fragment, create_text()); // TODO can we avoid this? + this.#outroing.delete(k); + this.#onscreen.delete(k); + }; - this.#offscreen.set(k, { effect, fragment }); + if (this.#transition || !onscreen) { + this.#outroing.add(k); + pause_effect(effect, on_destroy, false); } else { - destroy_effect(effect); + on_destroy(); } - - this.#outroing.delete(k); - this.#onscreen.delete(k); - }; - - if (this.#transition || !onscreen) { - this.#outroing.add(k); - pause_effect(effect, on_destroy, false); - } else { - on_destroy(); } + } finally { + pop_renderer?.(); } - - pop_renderer?.(); }; /** diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 4afc6a6ec0..d2418f9757 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -273,30 +273,32 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var pop_renderer = push_renderer(renderer); - state.pending.delete(batch); - - state.fallback = fallback; - reconcile(state, array, anchor, flags, get_key); - - if (fallback !== null) { - if (array.length === 0) { - if ((fallback.f & EFFECT_OFFSCREEN) === 0) { - resume_effect(fallback); + try { + state.pending.delete(batch); + + state.fallback = fallback; + reconcile(state, array, anchor, flags, get_key); + + if (fallback !== null) { + if (array.length === 0) { + if ((fallback.f & EFFECT_OFFSCREEN) === 0) { + resume_effect(fallback); + } else { + fallback.f ^= EFFECT_OFFSCREEN; + move(fallback, null, anchor); + } } else { - fallback.f ^= EFFECT_OFFSCREEN; - move(fallback, null, anchor); + pause_effect(fallback, () => { + // TODO only null out if no pending batch needs it, + // otherwise re-add `fallback.fragment` and move the + // effect into it + fallback = null; + }); } - } else { - pause_effect(fallback, () => { - // TODO only null out if no pending batch needs it, - // otherwise re-add `fallback.fragment` and move the - // effect into it - fallback = null; - }); } + } finally { + pop_renderer?.(); } - - pop_renderer?.(); } /** diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 7112f48f90..308d43bf57 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -753,17 +753,19 @@ export function move_effect(effect, fragment) { var pop_renderer = push_renderer(effect.r); - /** @type {TemplateNode | null} */ - var node = effect.nodes.start; - var end = effect.nodes.end; - - while (node !== null) { + try { /** @type {TemplateNode | null} */ - var next = node === end ? null : get_next_sibling(node); + var node = effect.nodes.start; + var end = effect.nodes.end; - append_child(fragment, node); - node = next; - } + while (node !== null) { + /** @type {TemplateNode | null} */ + var next = node === end ? null : get_next_sibling(node); - pop_renderer?.(); + append_child(fragment, node); + node = next; + } + } finally { + pop_renderer?.(); + } }