From d793d570e25c5fe6dde0980b96925b6fa274aa12 Mon Sep 17 00:00:00 2001
From: Dominic Gannaway <trueadm@users.noreply.github.com>
Date: Wed, 6 Dec 2023 15:48:33 +0000
Subject: [PATCH] fix: improve consistency issues around binding invalidation
 (#9810)

* co

* Add comment
---
 .changeset/soft-clocks-remember.md               |  5 +++++
 .../3-transform/client/visitors/template.js      |  2 +-
 packages/svelte/src/internal/client/runtime.js   | 11 +++++++++++
 packages/svelte/src/internal/index.js            |  1 +
 .../samples/invalidate-effect/_config.js         | 16 ++++++++++++++++
 .../samples/invalidate-effect/main.svelte        | 13 +++++++++++++
 6 files changed, 47 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/soft-clocks-remember.md
 create mode 100644 packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte

diff --git a/.changeset/soft-clocks-remember.md b/.changeset/soft-clocks-remember.md
new file mode 100644
index 0000000000..69e8aca06e
--- /dev/null
+++ b/.changeset/soft-clocks-remember.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: improve consistency issues around binding invalidation
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
index 6579d17c01..6e3e570454 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
@@ -245,7 +245,7 @@ function setup_select_synchronization(value_binding, context) {
 	context.state.init.push(
 		b.stmt(
 			b.call(
-				'$.pre_effect',
+				'$.invalidate_effect',
 				b.thunk(
 					b.block([
 						b.stmt(
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 5d92c436e8..f075d2d5a3 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -1273,6 +1273,17 @@ export function pre_effect(init) {
 	);
 }
 
+/**
+ * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the
+ * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything.
+ *
+ * @param {() => void | (() => void)} init
+ * @returns {import('./types.js').EffectSignal}
+ */
+export function invalidate_effect(init) {
+	return internal_create_effect(PRE_EFFECT, init, true, current_block, true);
+}
+
 /**
  * @param {() => void | (() => void)} init
  * @returns {import('./types.js').EffectSignal}
diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js
index 4d3205b824..a94a0de180 100644
--- a/packages/svelte/src/internal/index.js
+++ b/packages/svelte/src/internal/index.js
@@ -12,6 +12,7 @@ export {
 	user_effect,
 	render_effect,
 	pre_effect,
+	invalidate_effect,
 	flushSync,
 	bubble_event,
 	safe_equal,
diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
new file mode 100644
index 0000000000..b4ba660ad4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
@@ -0,0 +1,16 @@
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		assert.htmlEqual(target.innerHTML, 'a\n<select></select><button>change</button');
+
+		const [b1] = target.querySelectorAll('button');
+		b1.click();
+		await Promise.resolve();
+
+		assert.htmlEqual(
+			target.innerHTML,
+			'a\n<select></select>b\n<select></select><button>change</button'
+		);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte
new file mode 100644
index 0000000000..895a940451
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/main.svelte
@@ -0,0 +1,13 @@
+<script>
+	let entries = $state([{selected: 'a'}])
+</script>
+
+{#each entries as entry}
+	{entry.selected} <select bind:value={entry.selected}></select>
+{/each}
+
+<button
+	on:click={
+		() => entries = [{selected: 'a'}, {selected: 'b'}]
+	}
+	>change</button>