From 6a7e033be143a45d05c1e33a1f35e9720a4669bc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Oct 2025 16:12:55 -0700 Subject: [PATCH 1/3] fix: coordinate mount of snippets with await expressions --- .changeset/huge-poets-tickle.md | 5 ++++ .../internal/client/dom/blocks/boundary.js | 30 +++++++++++++++++-- .../Child.svelte | 7 +++++ .../_config.js | 25 ++++++++++++++++ .../main.svelte | 21 +++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 .changeset/huge-poets-tickle.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md new file mode 100644 index 000000000000..f2b1ba6f25a3 --- /dev/null +++ b/.changeset/huge-poets-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: coordinate mount of snippets with await expressions diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 72e64b1a3aa0..7df0cfca44c0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -38,6 +38,7 @@ import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; +import { create_text } from '../operations.js'; /** * @typedef {{ @@ -92,6 +93,9 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; + /** @type {TemplateNode | null} */ + #pending_anchor = null; + #local_pending_count = 0; #pending_count = 0; @@ -155,8 +159,17 @@ export class Boundary { this.#hydrate_resolved_content(); } } else { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + try { - this.#main_effect = branch(() => children(this.#anchor)); + this.#main_effect = branch(() => children(anchor)); } catch (error) { this.error(error); } @@ -165,6 +178,7 @@ export class Boundary { this.#show_pending_snippet(); } else { this.#pending = false; + this.#pending_anchor?.remove(); } } }, flags); @@ -194,9 +208,18 @@ export class Boundary { this.#pending_effect = branch(() => pending(this.#anchor)); Batch.enqueue(() => { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(this.#anchor)); + return branch(() => this.#children(anchor)); }); if (this.#pending_count > 0) { @@ -207,6 +230,7 @@ export class Boundary { }); this.#pending = false; + this.#pending_anchor?.remove(); } }); } @@ -252,6 +276,7 @@ export class Boundary { 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); } @@ -287,6 +312,7 @@ export class Boundary { } if (this.#offscreen_fragment) { + this.#pending_anchor?.remove(); this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte new file mode 100644 index 000000000000..7085219a5af6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte @@ -0,0 +1,7 @@ + + +

message: {message}

+{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js new file mode 100644 index 000000000000..b6ca2ae3d2a1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, `

loading...

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

message: hello from child

+

hello from parent

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte new file mode 100644 index 000000000000..3ad2c9572a47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte @@ -0,0 +1,21 @@ + + + + + + +

{await push('hello from parent')}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 1740624fe2073926b2f242a759ac573f37586798 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Oct 2025 16:44:00 -0700 Subject: [PATCH 2/3] try this --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 7df0cfca44c0..0855238cf677 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -178,9 +178,12 @@ export class Boundary { this.#show_pending_snippet(); } else { this.#pending = false; - this.#pending_anchor?.remove(); } } + + return () => { + this.#pending_anchor?.remove(); + }; }, flags); if (hydrating) { @@ -230,7 +233,6 @@ export class Boundary { }); this.#pending = false; - this.#pending_anchor?.remove(); } }); } @@ -312,7 +314,6 @@ export class Boundary { } if (this.#offscreen_fragment) { - this.#pending_anchor?.remove(); this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } From d2ab978a7d918069f6685e13a13dc958d707f8e6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:25:03 +0200 Subject: [PATCH 3/3] deduplicate --- .../internal/client/dom/blocks/boundary.js | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 0855238cf677..febbc00898de 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -159,14 +159,7 @@ export class Boundary { this.#hydrate_resolved_content(); } } else { - var anchor = this.#anchor; - - if (this.#pending) { - this.#pending_anchor = create_text(); - this.#anchor.before(this.#pending_anchor); - - anchor = this.#pending_anchor; - } + var anchor = this.#get_anchor(); try { this.#main_effect = branch(() => children(anchor)); @@ -211,14 +204,7 @@ export class Boundary { this.#pending_effect = branch(() => pending(this.#anchor)); Batch.enqueue(() => { - var anchor = this.#anchor; - - if (this.#pending) { - this.#pending_anchor = create_text(); - this.#anchor.before(this.#pending_anchor); - - anchor = this.#pending_anchor; - } + var anchor = this.#get_anchor(); this.#main_effect = this.#run(() => { Batch.ensure(); @@ -237,6 +223,19 @@ export class Boundary { }); } + #get_anchor() { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + + return anchor; + } + /** * Returns `true` if the effect exists inside a boundary whose pending snippet is shown * @returns {boolean}