diff --git a/.changeset/yellow-dodos-smell.md b/.changeset/yellow-dodos-smell.md
new file mode 100644
index 000000000000..ea2aead6622e
--- /dev/null
+++ b/.changeset/yellow-dodos-smell.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: properly add owners to function bindings
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
index 068971145c6e..e3492bfebe57 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
@@ -2,7 +2,11 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
-import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
+import {
+ extract_all_identifiers_from_expression,
+ get_attribute_chunks,
+ object
+} from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { create_derived } from '../../utils.js';
import { build_bind_this, validate_binding } from '../shared/utils.js';
@@ -93,6 +97,8 @@ export function build_component(node, component_name, context, anchor = context.
}
}
+ const ownerships_effects = new Map();
+
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
@@ -185,17 +191,23 @@ export function build_component(node, component_name, context, anchor = context.
// Only run ownership addition on $state fields.
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
- if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') {
- binding_initializers.push(
- b.stmt(
- b.call(
- b.id('$.add_owner_effect'),
- b.thunk(expression),
- b.id(component_name),
- is_ignored(node, 'ownership_invalid_binding') && b.true
+ if (
+ binding?.kind !== 'derived' &&
+ binding?.kind !== 'raw_state' &&
+ !ownerships_effects.has(left?.name)
+ ) {
+ ownerships_effects.set(left?.name, () => {
+ binding_initializers.push(
+ b.stmt(
+ b.call(
+ b.id('$.add_owner_effect'),
+ b.thunk(expression),
+ b.id(component_name),
+ is_ignored(node, 'ownership_invalid_binding') && b.true
+ )
)
- )
- );
+ );
+ });
}
}
@@ -212,6 +224,32 @@ export function build_component(node, component_name, context, anchor = context.
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
+ if (dev) {
+ const [, get_ids] = extract_all_identifiers_from_expression(get);
+
+ for (let get_id of get_ids) {
+ const binding = context.state.scope.get(get_id.name);
+ if (
+ binding &&
+ binding.kind !== 'derived' &&
+ binding.kind !== 'raw_state' &&
+ !ownerships_effects.has(get_id.name)
+ ) {
+ ownerships_effects.set(get_id.name, () => {
+ binding_initializers.push(
+ b.stmt(
+ b.call(
+ b.id('$.add_owner_effect'),
+ b.thunk(get_id),
+ b.id(component_name),
+ is_ignored(node, 'ownership_invalid_binding') && b.true
+ )
+ )
+ );
+ });
+ }
+ }
+ }
}
} else {
if (
@@ -255,6 +293,10 @@ export function build_component(node, component_name, context, anchor = context.
}
}
+ for (let [, ownership_effect] of ownerships_effects) {
+ ownership_effect();
+ }
+
delayed_props.forEach((fn) => fn());
if (slot_scope_applies_to_itself) {
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/Child.svelte
new file mode 100644
index 000000000000..ef91b0756dfb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/Child.svelte
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/_config.js
new file mode 100644
index 000000000000..4c77aea20684
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ test({ target, warnings, assert }) {
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(warnings, []);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(warnings, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/main.svelte
new file mode 100644
index 000000000000..4a3ce82726b6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/main.svelte
@@ -0,0 +1,10 @@
+
+
+ len % 2 === 0 ? arr : arr2, (v) => {}} />
\ No newline at end of file