fix: abort deriveds own AbortSignal when it disconnects (#18400)

Fixes https://github.com/sveltejs/svelte/issues/18301

---------

Co-authored-by: Fedor Nezhivoi <f.nezhivoi@corp.vk.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/18497/head
Fedor Nezhivoi 1 week ago committed by GitHub
parent 4da9f74cd9
commit a91b068786
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: abort deriveds own AbortSignal when it disconnects

@ -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);

@ -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);

@ -0,0 +1,20 @@
<script lang="ts">
import { getAbortSignal } from "svelte";
let { count, aborted = $bindable() } = $props()
let der = $derived.by(()=>{
const signal = getAbortSignal();
signal.addEventListener("abort", () => {
try {
aborted++;
} catch(e) {
console.error(e);
}
});
return count;
})
</script>
{der}

@ -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 <button>increment</button>');
assert.deepEqual(errors, []);
}
});

@ -0,0 +1,14 @@
<script lang="ts">
import Child from './Child.svelte'
let count = $state(0);
let aborted = $state(0)
</script>
{aborted}
<button onclick={() => count++}>increment</button>
{#if count % 2 === 0}
<Child {count} bind:aborted={aborted} />
{/if}

@ -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 <button></button>');
assert.deepEqual(errors, []);
}
});

@ -9,9 +9,9 @@
const signal = getAbortSignal();
signal.addEventListener("abort", () => {
try{
try {
aborted++;
}catch(e){
} catch(e) {
console.error(e);
}
});
@ -19,6 +19,6 @@
})
</script>
{der}
{der}:{aborted}
<button onclick={() => count++}></button>
<button onclick={() => count++}></button>

Loading…
Cancel
Save