diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index efd5628ae9..c76eacbf1b 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,14 @@ if (DEV) { throw_rune_error('$bindable'); } +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 ccc853c3bc..79b98e3577 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -30,3 +30,5 @@ export const EFFECT_ASYNC = 1 << 25; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); + +export const STALE_REACTION = Symbol('stale reaction'); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c508e515c0..44e51b412f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,6 +9,7 @@ import { EFFECT_ASYNC, EFFECT_PRESERVED, MAYBE_DIRTY, + STALE_REACTION, UNOWNED } from '#client/constants'; import { @@ -33,6 +34,7 @@ import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; +import { noop } from '../../shared/utils.js'; /** @type {Effect | null} */ export let from_async_derived = null; @@ -77,7 +79,8 @@ export function derived(fn) { rv: 0, v: /** @type {V} */ (null), wv: 0, - parent: parent_derived ?? active_effect + parent: parent_derived ?? active_effect, + ac: null }; if (DEV && tracing_mode_flag) { @@ -177,7 +180,17 @@ export function async_derived(fn, location) { (e) => { prev = null; - handle_error(e, parent, null, parent.ctx); + if (e === STALE_REACTION) { + if (should_suspend) { + if (!ran) { + boundary.decrement(); + } else { + batch.decrement(); + } + } + } else { + handle_error(e, parent, null, parent.ctx); + } } ); }, EFFECT_ASYNC | EFFECT_PRESERVED); @@ -185,7 +198,7 @@ export function async_derived(fn, location) { return new Promise((fulfil) => { /** @param {Promise} p */ function next(p) { - p.then(() => { + function go() { if (p === promise) { fulfil(signal); } else { @@ -193,7 +206,9 @@ export function async_derived(fn, location) { // resolves, delay resolution until we have a value next(promise); } - }); + } + + p.then(go, go); } next(promise); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 704633b39c..051b3f741f 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -31,7 +31,8 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_PRESERVED + EFFECT_PRESERVED, + STALE_REACTION } from '#client/constants'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -112,7 +113,8 @@ function create_effect(type, fn, sync, push = true) { prev: null, teardown: null, transitions: null, - wv: 0 + wv: 0, + ac: null }; if (DEV) { @@ -425,6 +427,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) { @@ -502,6 +506,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 6c665bbbe1..5af392c791 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -32,6 +32,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 43ceb408bd..4accdb0ce6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -26,7 +26,8 @@ import { REACTION_IS_UPDATING, EFFECT_IS_UPDATING, EFFECT_ASYNC, - RENDER_EFFECT + RENDER_EFFECT, + STALE_REACTION } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -439,6 +440,11 @@ export function update_reaction(reaction) { reaction.f |= EFFECT_IS_UPDATING; + if (reaction.ac !== null) { + reaction.ac?.abort(STALE_REACTION); + reaction.ac = null; + } + try { reaction.f |= REACTION_IS_UPDATING; var result = /** @type {Function} */ (0, reaction.fn)(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js new file mode 100644 index 0000000000..1405ee6e9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -0,0 +1,37 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs, variant }) { + if (variant === 'hydrate') { + await Promise.resolve(); + } + + const [reset, resolve] = target.querySelectorAll('button'); + + flushSync(() => reset.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.deepEqual(logs, ['aborted']); + + flushSync(() => resolve.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte new file mode 100644 index 0000000000..d8d77bf0e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte @@ -0,0 +1,29 @@ + + + + + + +

{await load(deferred)}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 4b88ecb58c..e437bb6bab 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -348,6 +348,7 @@ declare module 'svelte' { */ props: Props; }); + export function getAbortSignal(): AbortSignal; /** * `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.