From 128be7625ebaef39bc95afd48249abcceced1de5 Mon Sep 17 00:00:00 2001
From: Nic <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Fri, 12 Jun 2026 21:03:42 -0400
Subject: [PATCH 1/3] fix: don't resurrect outroing elements when an ancestor
block is paused and resumed
---
.changeset/quiet-falcons-search.md | 5 ++++
.../svelte/src/internal/client/constants.js | 7 +++++
.../src/internal/client/reactivity/effects.js | 9 +++++-
.../_config.js | 29 +++++++++++++++++++
.../main.svelte | 19 ++++++++++++
5 files changed, 68 insertions(+), 1 deletion(-)
create mode 100644 .changeset/quiet-falcons-search.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte
diff --git a/.changeset/quiet-falcons-search.md b/.changeset/quiet-falcons-search.md
new file mode 100644
index 0000000000..8c5c4dedca
--- /dev/null
+++ b/.changeset/quiet-falcons-search.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't resurrect outroing elements when an ancestor block is paused and resumed
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 043b50b4b2..974e9d00ef 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -21,6 +21,13 @@ export const BOUNDARY_EFFECT = 1 << 7;
* no dependents, we can disconnect it from the graph, allowing it to either be
* GC'd or reconnected later if an effect comes to depend on it again
*/
+/**
+ * Set on the effect that `pause_effect` was called on, i.e. the root of a paused subtree,
+ * as opposed to its descendants which are merely `INERT`. This allows `resume_effect` on
+ * an ancestor to skip subtrees that were paused for their own reasons (such as a block
+ * whose condition is still false) rather than resurrecting them
+ */
+export const PAUSED = 1 << 8;
export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index c5d195dfae..f6f4e385f0 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -34,7 +34,8 @@ import {
ASYNC,
CONNECTED,
MANAGED_EFFECT,
- DESTROYING
+ DESTROYING,
+ PAUSED
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@@ -610,6 +611,7 @@ export function pause_effect(effect, callback, destroy = true) {
/** @type {TransitionManager[]} */
var transitions = [];
+ effect.f |= PAUSED;
pause_children(effect, transitions, true);
var fn = () => {
@@ -677,6 +679,7 @@ function pause_children(effect, transitions, local) {
* @param {Effect} effect
*/
export function resume_effect(effect) {
+ effect.f &= ~PAUSED;
resume_children(effect, true);
}
@@ -685,6 +688,10 @@ export function resume_effect(effect) {
* @param {boolean} local
*/
function resume_children(effect, local) {
+ // this subtree was paused for its own reasons (e.g. a block whose condition
+ // is still false) — its controller will resume or destroy it
+ if ((effect.f & PAUSED) !== 0) return;
+
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js
new file mode 100644
index 0000000000..4009eea204
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js
@@ -0,0 +1,29 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+// #15604 — an element mid-outro must not be resurrected when an ancestor
+// block is paused and resumed while the element's own condition is still false.
+// The outer `transition:fade` matters: it keeps the outer branch alive (pending
+// its own outro) so that toggling back takes the resume path
+export default test({
+ test({ assert, target, raf }) {
+ const [hide, fetch] = target.querySelectorAll('button');
+
+ // let the mount intro (dom variant) finish
+ raf.tick(200);
+ assert.ok(target.querySelector('.red'));
+
+ // start the outro of the inner block
+ flushSync(() => hide.click());
+
+ // pause the outer block while the outro is in flight...
+ flushSync(() => fetch.click());
+ raf.tick(250);
+
+ // ...then resume it before either outro completes
+ flushSync(() => fetch.click());
+ raf.tick(2000);
+
+ assert.equal(target.querySelector('.red'), null);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte
new file mode 100644
index 0000000000..0060401b8e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+
+{#if fetching}
+
loading
+{:else}
+
+ {#if shown}
+
red
+ {/if}
+
+{/if}
From f77cebf37ff7481cc192fd29c90ab64425d4b1c3 Mon Sep 17 00:00:00 2001
From: Nic <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Fri, 10 Jul 2026 18:53:19 -0400
Subject: [PATCH 2/3] test: cover the each.js outro path and the
flip-back-while-paused case
---
.../_config.js | 23 ++++++++++++++++++
.../main.svelte | 19 +++++++++++++++
.../_config.js | 24 +++++++++++++++++++
.../main.svelte | 19 +++++++++++++++
4 files changed, 85 insertions(+)
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js
new file mode 100644
index 0000000000..7ccfd72f00
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+// #15604 — a removed each item mid-outro must not be resurrected by an ancestor pause/resume
+export default test({
+ test({ assert, target, raf }) {
+ const [remove, fetch] = target.querySelectorAll('button');
+
+ raf.tick(200);
+ assert.equal(target.querySelectorAll('.item').length, 3);
+
+ flushSync(() => remove.click());
+
+ flushSync(() => fetch.click());
+ raf.tick(30);
+
+ flushSync(() => fetch.click());
+ raf.tick(2000);
+
+ const items = [...target.querySelectorAll('.item')].map((el) => el.textContent);
+ assert.deepEqual(items, ['a', 'c']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte
new file mode 100644
index 0000000000..36e685e3c8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+
+{#if fetching}
+
loading
+{:else}
+
+ {#each items as item (item)}
+
{item}
+ {/each}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js
new file mode 100644
index 0000000000..a8e5e99bbf
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js
@@ -0,0 +1,24 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+// #15604 companion — flipping the condition back while the ancestor is paused must still revive the element
+export default test({
+ test({ assert, target, raf }) {
+ const [toggle, fetch] = target.querySelectorAll('button');
+
+ raf.tick(200);
+ assert.ok(target.querySelector('.red'));
+
+ flushSync(() => toggle.click());
+
+ flushSync(() => fetch.click());
+ raf.tick(30);
+
+ flushSync(() => toggle.click());
+
+ flushSync(() => fetch.click());
+ raf.tick(2000);
+
+ assert.ok(target.querySelector('.red'));
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte
new file mode 100644
index 0000000000..71a3ea2090
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+
+{#if fetching}
+
loading
+{:else}
+
+ {#if shown}
+
red
+ {/if}
+
+{/if}
From ec46115e910e353e6116863eca5c3b8a8b4f1a6a Mon Sep 17 00:00:00 2001
From: Nic <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Fri, 24 Jul 2026 20:18:07 -0400
Subject: [PATCH 3/3] chore: keep the CONNECTED comment attached to its
constant
---
packages/svelte/src/internal/client/constants.js | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 974e9d00ef..3232cbaf16 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -15,12 +15,6 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
-/**
- * Indicates that a reaction is connected to an effect root — either it is an effect,
- * or it is a derived that is depended on by at least one effect. If a derived has
- * no dependents, we can disconnect it from the graph, allowing it to either be
- * GC'd or reconnected later if an effect comes to depend on it again
- */
/**
* Set on the effect that `pause_effect` was called on, i.e. the root of a paused subtree,
* as opposed to its descendants which are merely `INERT`. This allows `resume_effect` on
@@ -28,6 +22,12 @@ export const BOUNDARY_EFFECT = 1 << 7;
* whose condition is still false) rather than resurrecting them
*/
export const PAUSED = 1 << 8;
+/**
+ * Indicates that a reaction is connected to an effect root — either it is an effect,
+ * or it is a derived that is depended on by at least one effect. If a derived has
+ * no dependents, we can disconnect it from the graph, allowing it to either be
+ * GC'd or reconnected later if an effect comes to depend on it again
+ */
export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;