From bc10186b69074cbd3aa0fce3f517afbf4ab84705 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 09:10:36 -0400 Subject: [PATCH] WIP getAbortSignal --- packages/svelte/src/index-client.js | 33 ++++++++++++++++++- packages/svelte/src/index-server.js | 15 +++++++++ .../svelte/src/internal/client/constants.js | 2 ++ .../src/internal/client/reactivity/effects.js | 6 +++- .../src/internal/client/reactivity/types.d.ts | 2 ++ .../svelte/src/internal/client/runtime.js | 8 ++++- 6 files changed, 63 insertions(+), 3 deletions(-) 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;