From 7e6a3bfea8f7023d725288c9ef82cb67d810ffe0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 9 Sep 2025 22:05:25 -0400 Subject: [PATCH 01/14] fix: flush effects scheduled during boundary's pending phase --- .changeset/ninety-pandas-move.md | 5 +++++ .../src/internal/client/dom/blocks/boundary.js | 10 +++++++++- .../samples/async-attachment/Inner.svelte | 10 ++++++++++ .../samples/async-attachment/_config.js | 18 ++++++++++++++++++ .../samples/async-attachment/main.svelte | 16 ++++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/ninety-pandas-move.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte diff --git a/.changeset/ninety-pandas-move.md b/.changeset/ninety-pandas-move.md new file mode 100644 index 0000000000..65f57ddbbf --- /dev/null +++ b/.changeset/ninety-pandas-move.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush effects scheduled during boundary's pending phase diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index b7f1803782..659f6014b8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -23,7 +23,7 @@ import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { DEV } from 'esm-env'; -import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; +import { Batch, current_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'; @@ -259,6 +259,14 @@ export class Boundary { this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } + + const batch = current_batch; + + if (batch) { + queue_micro_task(() => { + batch.flush(); + }); + } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte new file mode 100644 index 0000000000..b9b9d7a3d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte @@ -0,0 +1,10 @@ + + +

{test}

+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attachment/_config.js new file mode 100644 index 0000000000..f6b48b38b1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

foo

foo
'); + + const [toggle] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + + toggle.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

foo

foo
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte new file mode 100644 index 0000000000..6cef6e8f5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte @@ -0,0 +1,16 @@ + + + + + {#if show} + + {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
From bcd3acad210c22b8c39d6408e94207ee1754cf21 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 9 Sep 2025 22:22:22 -0400 Subject: [PATCH 02/14] simpler fix --- .../svelte/src/internal/client/dom/blocks/boundary.js | 8 -------- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 659f6014b8..28f35b0b66 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -259,14 +259,6 @@ export class Boundary { this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } - - const batch = current_batch; - - if (batch) { - queue_micro_task(() => { - batch.flush(); - }); - } } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 5176a4f74b..555286e53a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -685,7 +685,7 @@ export function suspend() { batch.activate(); batch.decrement(); } else { - batch.deactivate(); + batch.flush(); } unset_context(); From e36137d161e88013f116e5b9e2b141d7d86ca1a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 10 Sep 2025 08:18:54 -0400 Subject: [PATCH 03/14] add async-effect-after-await test --- .../samples/async-effect-after-await/Child.svelte | 7 +++++++ .../samples/async-effect-after-await/_config.js | 9 +++++++++ .../samples/async-effect-after-await/main.svelte | 9 +++++++++ 3 files changed, 25 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte new file mode 100644 index 0000000000..682f7a0631 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js new file mode 100644 index 0000000000..81548a25ea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, logs }) { + await tick(); + assert.deepEqual(logs, ['hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte new file mode 100644 index 0000000000..d4b67f8803 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte @@ -0,0 +1,9 @@ + + + + + + {#snippet pending()}{/snippet} + From f41dad72226104a417149442323df39126925dfd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 10 Sep 2025 12:33:08 -0400 Subject: [PATCH 04/14] failing test --- .../samples/async-effect-after-await/Child.svelte | 6 +++++- .../samples/async-effect-after-await/_config.js | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte index 682f7a0631..758ebc0004 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte @@ -1,7 +1,11 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js index 81548a25ea..0908b6a9fe 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js @@ -3,7 +3,8 @@ import { test } from '../../test'; export default test({ async test({ assert, logs }) { + assert.deepEqual(logs, []); await tick(); - assert.deepEqual(logs, ['hello']); + assert.deepEqual(logs, ['before', 'after']); } }); From d50b4d955a8633aceeddad5711f65434e02c3a39 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 22 Sep 2025 14:58:02 -0400 Subject: [PATCH 05/14] add another test --- .../async-boundary-coordination/_config.js | 46 +++++++++++++++++++ .../async-boundary-coordination/main.svelte | 35 ++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js new file mode 100644 index 0000000000..f3e60e7b32 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

hello from server

+

hello from server

+

hello from server

+

hello from server

+ ` + ); + + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

hello from browser

+

hello from browser

+

hello from server

+

hello from server

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

hello from browser

+

hello from browser

+

hello from browser

+

hello from browser

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte new file mode 100644 index 0000000000..3986c45046 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte @@ -0,0 +1,35 @@ + + + + + + + + {#if await a.promise} +

hello from {browser ? 'browser' : 'server'}

+ {/if} + +

hello from {browser ? 'browser' : 'server'}

+
+ + + {#if await b.promise} +

hello from {browser ? 'browser' : 'server'}

+ {/if} + +

hello from {browser ? 'browser' : 'server'}

+
From ea29258bb0ad6c492a1c75785b08515155b1ee29 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 13 Feb 2026 17:14:32 -0500 Subject: [PATCH 06/14] fix test --- .../async-boundary-coordination/_config.js | 16 ++++++++++++++++ .../async-boundary-coordination/main.svelte | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js index f3e60e7b32..15e4ab45ef 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/_config.js @@ -2,12 +2,24 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + mode: ['hydrate'], + + props: { + browser: true + }, + + server_props: { + browser: false + }, + async test({ assert, target }) { await tick(); assert.htmlEqual( target.innerHTML, ` + +

hello from server

hello from server

hello from server

@@ -23,6 +35,8 @@ export default test({ assert.htmlEqual( target.innerHTML, ` + +

hello from browser

hello from browser

hello from server

@@ -36,6 +50,8 @@ export default test({ assert.htmlEqual( target.innerHTML, ` + +

hello from browser

hello from browser

hello from browser

diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte index 3986c45046..e837aac030 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-coordination/main.svelte @@ -1,5 +1,5 @@ + + + + + + +

{await push('resolved')}

+ + {#snippet pending()} +

{count}

+ {/snippet} +
From a3d46fc3627f9d5caa96e21df3751adc533c0bf1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 16 Feb 2026 11:16:28 -0500 Subject: [PATCH 14/14] i take it back, opencode didn't get it to work at all. revert revert revert --- .../internal/client/dom/blocks/boundary.js | 20 ++----------------- .../src/internal/client/reactivity/effects.js | 18 +---------------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 0f3c0057f9..44bd0f22f4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -37,7 +37,7 @@ import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { DEV } from 'esm-env'; -import { Batch, flushSync, schedule_effect } from '../../reactivity/batch.js'; +import { Batch, schedule_effect } 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'; @@ -164,9 +164,7 @@ export class Boundary { } else { this.#hydrate_resolved_content(); - // Match the non-hydrating logic: only stay pending if there's - // actual pending async work - if (this.#local_pending_count === 0) { + if (this.#pending_count === 0) { this.is_pending = false; } } @@ -263,14 +261,6 @@ export class Boundary { return !!this.#props.pending; } - /** - * Returns true if there's pending async work in this boundary - * @returns {boolean} - */ - has_pending_async() { - return this.#local_pending_count > 0; - } - /** * @param {() => Effect | null} fn */ @@ -374,12 +364,6 @@ export class Boundary { this.#local_pending_count += d; - // async work completed — if we don't have a pending snippet, - // we need to reschedule deferred effects here - if (this.#local_pending_count === 0 && !this.has_pending_snippet()) { - this.#reschedule_deferred_effects(); - } - if (!this.#effect_pending || this.#pending_count_update_queued) return; this.#pending_count_update_queued = true; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 3893c8391e..512c435a27 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -1,4 +1,3 @@ -/** @import { Boundary } from '../dom/blocks/boundary' */ /** @import { Blocker, ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ import { is_dirty, @@ -42,8 +41,6 @@ import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js'; import { Batch, current_batch, schedule_effect } from './batch.js'; -import { hydrating } from '../dom/hydration.js'; -import { async_mode_flag } from '../../flags/index.js'; import { flatten } from './async.js'; import { without_reactive_context } from '../dom/elements/bindings/shared.js'; import { set_signal_status } from './status.js'; @@ -122,18 +119,7 @@ function create_effect(type, fn, sync) { effect.component_function = dev_current_component_function; } - // During hydration, defer template effects until local promises have resolved - var should_defer = - async_mode_flag && - hydrating && - fn !== null && - (type & RENDER_EFFECT) !== 0 && - effect.b?.has_pending_async(); - - if (should_defer) { - // Store the effect in the boundary so it can be rescheduled when async work completes - /** @type {Boundary} */ (effect.b).defer_effect(effect); - } else if (sync) { + if (sync) { try { update_effect(effect); } catch (e) { @@ -150,10 +136,8 @@ function create_effect(type, fn, sync) { // if an effect has already ran and doesn't need to be kept in the tree // (because it won't re-run, has no DOM, and has no teardown etc) // then we skip it and go to its child (if any) - // NOTE: We only do this pruning if the effect actually ran (!should_defer) if ( sync && - !should_defer && e.deps === null && e.teardown === null && e.nodes === null &&