diff --git a/.changeset/many-dolls-argue.md b/.changeset/many-dolls-argue.md new file mode 100644 index 000000000000..54a9cef915f0 --- /dev/null +++ b/.changeset/many-dolls-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: run effects in pending snippets diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ef5f0e116d8e..da70cbb19d2a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,5 @@ /** @import { Effect, Source, TemplateNode, } from '#client' */ import { - BLOCK_EFFECT, BOUNDARY_EFFECT, COMMENT_NODE, DIRTY, @@ -53,7 +52,7 @@ import { set_signal_status } from '../../reactivity/status.js'; * }} BoundaryProps */ -var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; +var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED; /** * @param {TemplateNode} node @@ -98,15 +97,10 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; - /** @type {TemplateNode | null} */ - #pending_anchor = null; - #local_pending_count = 0; #pending_count = 0; #pending_count_update_queued = false; - #is_creating_fallback = false; - /** @type {Set} */ #dirty_effects = new Set(); @@ -142,51 +136,31 @@ export class Boundary { constructor(node, props, children) { this.#anchor = node; this.#props = props; - this.#children = children; - this.parent = /** @type {Effect} */ (active_effect).b; + this.#children = (anchor) => { + var effect = /** @type {Effect} */ (active_effect); - this.is_pending = !!this.#props.pending; + effect.b = this; + effect.f |= BOUNDARY_EFFECT; - this.#effect = block(() => { - /** @type {Effect} */ (active_effect).b = this; + children(anchor); + }; + + this.parent = /** @type {Effect} */ (active_effect).b; + this.#effect = block(() => { if (hydrating) { - const comment = this.#hydrate_open; + const comment = /** @type {Comment} */ (this.#hydrate_open); hydrate_next(); - const server_rendered_pending = - /** @type {Comment} */ (comment).nodeType === COMMENT_NODE && - /** @type {Comment} */ (comment).data === HYDRATION_START_ELSE; - - if (server_rendered_pending) { + if (comment.data === HYDRATION_START_ELSE) { this.#hydrate_pending_content(); } else { this.#hydrate_resolved_content(); - - if (this.#pending_count === 0) { - this.is_pending = false; - } } } else { - var anchor = this.#get_anchor(); - - try { - this.#main_effect = branch(() => children(anchor)); - } catch (error) { - this.error(error); - } - - if (this.#pending_count > 0) { - this.#show_pending_snippet(); - } else { - this.is_pending = false; - } + this.#render(); } - - return () => { - this.#pending_anchor?.remove(); - }; }, flags); if (hydrating) { @@ -206,19 +180,24 @@ export class Boundary { const pending = this.#props.pending; if (!pending) return; + this.is_pending = true; this.#pending_effect = branch(() => pending(this.#anchor)); queue_micro_task(() => { - var anchor = this.#get_anchor(); + var fragment = (this.#offscreen_fragment = document.createDocumentFragment()); + var anchor = create_text(); + + fragment.append(anchor); this.#main_effect = this.#run(() => { Batch.ensure(); return branch(() => this.#children(anchor)); }); - if (this.#pending_count > 0) { - this.#show_pending_snippet(); - } else { + if (this.#pending_count === 0) { + this.#anchor.before(fragment); + this.#offscreen_fragment = null; + pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { this.#pending_effect = null; }); @@ -228,17 +207,28 @@ export class Boundary { }); } - #get_anchor() { - var anchor = this.#anchor; + #render() { + try { + this.is_pending = this.has_pending_snippet(); + this.#pending_count = 0; + this.#local_pending_count = 0; + + this.#main_effect = branch(() => { + this.#children(this.#anchor); + }); - if (this.is_pending) { - this.#pending_anchor = create_text(); - this.#anchor.before(this.#pending_anchor); + if (this.#pending_count > 0) { + var fragment = (this.#offscreen_fragment = document.createDocumentFragment()); + move_effect(this.#main_effect, fragment); - anchor = this.#pending_anchor; + const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending); + this.#pending_effect = branch(() => pending(this.#anchor)); + } else { + this.is_pending = false; + } + } catch (error) { + this.error(error); } - - return anchor; } /** @@ -262,7 +252,8 @@ export class Boundary { } /** - * @param {() => Effect | null} fn + * @template T + * @param {() => T} fn */ #run(fn) { var previous_effect = active_effect; @@ -285,20 +276,6 @@ export class Boundary { } } - #show_pending_snippet() { - const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending); - - if (this.#main_effect !== null) { - this.#offscreen_fragment = document.createDocumentFragment(); - this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor)); - move_effect(this.#main_effect, this.#offscreen_fragment); - } - - if (this.#pending_effect === null) { - this.#pending_effect = branch(() => pending(this.#anchor)); - } - } - /** * Updates the pending count associated with the currently visible pending snippet, * if any, such that we can replace the snippet with content once work is done @@ -383,7 +360,7 @@ export class Boundary { // If we have nothing to capture the error, or if we hit an error while // rendering the fallback, re-throw for another boundary to handle - if (this.#is_creating_fallback || (!onerror && !failed)) { + if (!onerror && !failed) { throw error; } @@ -423,31 +400,18 @@ export class Boundary { e.svelte_boundary_reset_onerror(); } - // If the failure happened while flushing effects, current_batch can be null - Batch.ensure(); - - this.#local_pending_count = 0; - if (this.#failed_effect !== null) { pause_effect(this.#failed_effect, () => { this.#failed_effect = null; }); } - // we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset - // but it would be really weird to show the parent's boundary on a child reset. - this.is_pending = this.has_pending_snippet(); + this.#run(() => { + // If the failure happened while flushing effects, current_batch can be null + Batch.ensure(); - this.#main_effect = this.#run(() => { - this.#is_creating_fallback = false; - return branch(() => this.#children(this.#anchor)); + this.#render(); }); - - if (this.#pending_count > 0) { - this.#show_pending_snippet(); - } else { - this.is_pending = false; - } }; queue_micro_task(() => { @@ -462,10 +426,16 @@ export class Boundary { if (failed) { this.#failed_effect = this.#run(() => { Batch.ensure(); - this.#is_creating_fallback = true; try { return branch(() => { + // errors in `failed` snippets cause the boundary to error again + // TODO Svelte 6: revisit this decision, most likely better to go to parent boundary instead + var effect = /** @type {Effect} */ (active_effect); + + effect.b = this; + effect.f |= BOUNDARY_EFFECT; + failed( this.#anchor, () => error, @@ -475,8 +445,6 @@ export class Boundary { } catch (error) { invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent)); return null; - } finally { - this.#is_creating_fallback = false; } }); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 82a2d4f484bf..b382a4e3a5da 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -293,16 +293,19 @@ export class Batch { } } - var parent = effect.parent; - effect = effect.next; - - while (effect === null && parent !== null) { - if (parent === pending_boundary) { + while (effect !== null) { + if (effect === pending_boundary) { pending_boundary = null; } - effect = parent.next; - parent = parent.parent; + var next = effect.next; + + if (next !== null) { + effect = next; + break; + } + + effect = effect.parent; } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live/_config.js b/packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live/_config.js new file mode 100644 index 000000000000..1d860cded54d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live/_config.js @@ -0,0 +1,38 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +

0

+ `, + + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

1

+ ` + ); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

resolved

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live/main.svelte new file mode 100644 index 000000000000..764fd20a51f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live/main.svelte @@ -0,0 +1,31 @@ + + + + + + + +

{await push('resolved')}

+ + {#snippet pending()} +

{count}

+ {/snippet} +