From a91b0687866c8b2f78692010702bb811bf27b9a8 Mon Sep 17 00:00:00 2001 From: Fedor Nezhivoi Date: Tue, 14 Jul 2026 16:40:30 +0700 Subject: [PATCH] fix: abort deriveds own AbortSignal when it disconnects (#18400) Fixes https://github.com/sveltejs/svelte/issues/18301 --------- Co-authored-by: Fedor Nezhivoi Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/early-crabs-rest.md | 5 +++++ .../internal/client/reactivity/deriveds.js | 9 +++++++-- .../svelte/src/internal/client/runtime.js | 8 ++++++++ .../abort-signal-derived-destroy/Child.svelte | 20 +++++++++++++++++++ .../abort-signal-derived-destroy/_config.js | 14 +++++++++++++ .../abort-signal-derived-destroy/main.svelte | 14 +++++++++++++ .../abort-signal-derived-set-state/_config.js | 2 ++ .../main.svelte | 8 ++++---- 8 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 .changeset/early-crabs-rest.md create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte 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 +