diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index efd5628ae9..d843426ce0 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
-import { untrack } from './internal/client/runtime.js';
+import { active_reaction, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
@@ -44,6 +44,37 @@ if (DEV) {
throw_rune_error('$bindable');
}
+/**
+ * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed.
+ *
+ * Must be called while a derived or effect is running.
+ *
+ * ```svelte
+ *
+ * ```
+ */
+export function getAbortSignal() {
+ if (active_reaction === null) {
+ throw new Error('TODO getAbortSignal can only be called inside a reaction');
+ }
+
+ return (active_reaction.ac ??= new AbortController()).signal;
+}
+
/**
* `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
* Unlike `$effect`, the provided function only runs once.
diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js
index 0f1aff8f5a..f4cb6f8c41 100644
--- a/packages/svelte/src/index-server.js
+++ b/packages/svelte/src/index-server.js
@@ -35,6 +35,21 @@ export function unmount() {
export async function tick() {}
+/** @type {AbortController | null} */
+let controller = null;
+
+export function getAbortSignal() {
+ if (controller === null) {
+ const c = (controller = new AbortController());
+ queueMicrotask(() => {
+ c.abort();
+ controller = null;
+ });
+ }
+
+ return controller.signal;
+}
+
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 3ca915f98e..9329dceb6f 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -27,6 +27,8 @@ export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
+export const STALE_REACTION = Symbol('stale reaction');
+
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 03d073781d..62d1b59ad0 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -32,7 +32,8 @@ import {
HEAD_EFFECT,
MAYBE_DIRTY,
EFFECT_HAS_DERIVED,
- BOUNDARY_EFFECT
+ BOUNDARY_EFFECT,
+ STALE_REACTION
} from '#client/constants';
import { set } from './sources.js';
import * as e from '../errors.js';
@@ -397,6 +398,8 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null;
while (effect !== null) {
+ effect.ac?.abort(STALE_REACTION);
+
var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) {
@@ -478,6 +481,7 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn =
effect.nodes_start =
effect.nodes_end =
+ effect.ac =
null;
}
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 756bb98f09..88c84f27fe 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -40,6 +40,8 @@ export interface Reaction extends Signal {
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
+ /** An AbortController that aborts when the signal is destroyed */
+ ac: null | AbortController;
}
export interface Derived extends Value, Reaction {
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index d057bfdf0d..c66ed05927 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -22,7 +22,8 @@ import {
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
- EFFECT_IS_UPDATING
+ EFFECT_IS_UPDATING,
+ STALE_REACTION
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js';
@@ -276,6 +277,11 @@ export function update_reaction(reaction) {
reaction.f |= EFFECT_IS_UPDATING;
+ if (reaction.ac !== null) {
+ reaction.ac?.abort(STALE_REACTION);
+ reaction.ac = null;
+ }
+
try {
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;