diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index ca29d5bfbe..06b453a668 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -216,6 +216,7 @@ export function flushSync(fn) { } export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; +export { getAbortSignal } from './internal/client/reactivity/abort-signal.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index eaffd07ce3..afd4550f4c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -19,7 +19,9 @@ import { reset_is_throwing_error, schedule_effect, check_dirtiness, - update_effect + update_effect, + invocation_version, + set_invocation_version } from '../../runtime.js'; import { hydrate_next, @@ -405,6 +407,7 @@ export function capture(track = true) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; + var previous_invocation_version = invocation_version; if (DEV && !track) { var was_from_async_derived = from_async_derived; @@ -415,6 +418,7 @@ export function capture(track = true) { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); + set_invocation_version(invocation_version); } else if (DEV) { set_from_async_derived(was_from_async_derived); } diff --git a/packages/svelte/src/internal/client/reactivity/abort-signal.js b/packages/svelte/src/internal/client/reactivity/abort-signal.js new file mode 100644 index 0000000000..7c5780834a --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/abort-signal.js @@ -0,0 +1,15 @@ +import { active_reaction, invocation_version } from '../runtime.js'; + +export function getAbortSignal() { + if (active_reaction === null) { + throw new Error('TODO'); + } + + var controller = (active_reaction.ctrl ??= new AbortController()); + + if (active_reaction.iv > invocation_version) { + controller.abort(); + } + + return controller.signal; +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2ab2908c77..7a0f8302b9 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -115,7 +115,9 @@ function create_effect(type, fn, sync, push = true) { prev: null, teardown: null, transitions: null, - wv: 0 + wv: 0, + iv: 0, + ctrl: null }; if (DEV) { diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 5ef0097649..f03a3f3afc 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -31,6 +31,10 @@ export interface Reaction extends Signal { fn: null | Function; /** Signals that this signal reads from */ deps: null | Value[]; + /** Invocation version, so we can know if we're in a reaction that was already invalidated */ + iv: number; + /** AbortController */ + ctrl: AbortController | null; } 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 8016eeb9b2..030331ab33 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -109,6 +109,13 @@ export function set_active_effect(effect) { active_effect = effect; } +export let invocation_version = 0; + +/** @param {number} v */ +export function set_invocation_version(v) { + invocation_version = v; +} + // TODO remove this, once we're satisfied that we're not leaking context /* @__PURE__ */ setInterval(() => { @@ -444,9 +451,13 @@ export function update_reaction(reaction) { set_component_context(reaction.ctx); untracking = false; read_version++; + invocation_version = ++reaction.iv; try { reaction.f |= REACTION_IS_UPDATING; + reaction.ctrl?.abort(); + reaction.ctrl = null; + var result = /** @type {Function} */ (0, reaction.fn)(); var deps = reaction.deps; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index bd9bc50ae3..8423903fa3 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,5 +1,5 @@ import { describe, assert, it } from 'vitest'; -import { flushSync } from '../../src/index-client'; +import { flushSync, getAbortSignal } from '../../src/index-client'; import * as $ from '../../src/internal/client/runtime'; import { push, pop } from '../../src/internal/client/context'; import { @@ -1012,4 +1012,42 @@ describe('signals', () => { destroy(); }; }); + + test('$effect can use getAbortSignal', () => { + return () => { + const n = state(0); + const abort_signals: AbortSignal[] = []; + + const destroy = effect_root(() => { + render_effect(() => { + $.get(n); + const signal = getAbortSignal(); + abort_signals.push(signal); + }); + }); + + assert.deepEqual( + abort_signals.map((s) => s.aborted), + [false] + ); + + set(n, 1); + flushSync(); + + assert.deepEqual( + abort_signals.map((s) => s.aborted), + [true, false] + ); + + set(n, 2); + flushSync(); + + assert.deepEqual( + abort_signals.map((s) => s.aborted), + [true, true, false] + ); + + destroy(); + }; + }); }); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c32882c132..1d22c8ddfb 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -412,6 +412,7 @@ declare module 'svelte' { * Synchronously flushes any pending state changes and those that result from it. * */ export function flushSync(fn?: (() => void) | undefined): void; + export function getAbortSignal(): AbortSignal; /** * Create a snippet programmatically * */