diff --git a/.changeset/early-crabs-rest.md b/.changeset/early-crabs-rest.md
new file mode 100644
index 0000000000..d3f290a5c2
--- /dev/null
+++ b/.changeset/early-crabs-rest.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: abort deriveds own AbortSignal when it disconnects
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index fd64f3b45d..6b48bc8a23 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -28,6 +28,7 @@ import {
skipped_deps,
new_deps
} from '../runtime.js';
+import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
@@ -450,14 +451,18 @@ export function freeze_derived_effects(derived) {
// if the effect has a teardown function or abort signal, call it
if (e.teardown || e.ac) {
e.teardown?.();
- e.ac?.abort(STALE_REACTION);
+ if (e.ac !== null) {
+ without_reactive_context(() => {
+ /** @type {AbortController} */ (e.ac).abort(STALE_REACTION);
+ e.ac = null;
+ });
+ }
// make it a noop so it doesn't get called again if the derived
// is unfrozen. we don't set it to `null`, because the existence
// of a teardown function is what determines whether the
// effect runs again during unfreezing (but not for teardown-only effects)
if (e.fn !== null) e.teardown = noop;
- e.ac = null;
remove_reactions(e, 0);
destroy_effect_children(e);
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 188d16a820..2ebce07916 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -408,6 +408,14 @@ function remove_reaction(signal, dependency) {
update_derived_status(derived);
}
+ // Call abort controller, noone's listening to this derived anymore
+ if (derived.ac !== null) {
+ without_reactive_context(() => {
+ /** @type {AbortController} */ (derived.ac).abort(STALE_REACTION);
+ derived.ac = null;
+ });
+ }
+
// freeze any effects inside this derived
freeze_derived_effects(derived);
diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte
new file mode 100644
index 0000000000..5a837e5ea4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte
@@ -0,0 +1,20 @@
+
+
+{der}
diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js
new file mode 100644
index 0000000000..3c6ec98fe6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js
@@ -0,0 +1,14 @@
+import { ok, test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, errors }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, '1 ');
+ assert.deepEqual(errors, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte
new file mode 100644
index 0000000000..57c467a80c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte
@@ -0,0 +1,14 @@
+
+
+{aborted}
+
+
+
+{#if count % 2 === 0}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js
index 2dacf188d7..aa38dd8280 100644
--- a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js
@@ -4,9 +4,11 @@ import { flushSync } from 'svelte';
export default test({
async test({ assert, target, errors }) {
const btn = target.querySelector('button');
+
flushSync(() => {
btn?.click();
});
+ assert.htmlEqual(target.innerHTML, '1:1 ');
assert.deepEqual(errors, []);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte
index ebefe38fb2..e76cecbac9 100644
--- a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte
@@ -9,9 +9,9 @@
const signal = getAbortSignal();
signal.addEventListener("abort", () => {
- try{
+ try {
aborted++;
- }catch(e){
+ } catch(e) {
console.error(e);
}
});
@@ -19,6 +19,6 @@
})
-{der}
+{der}:{aborted}
-
\ No newline at end of file
+