implement getAbortSignal

pull/16197/head
Rich Harris 5 months ago
parent 8baf1644a7
commit 666a148f64

@ -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.

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

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

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

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

@ -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<V = unknown> extends Value<V>, Reaction {

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

@ -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,
`
<button>reset</button>
<button>resolve</button>
<h1>hello</h1>
`
);
}
});

@ -0,0 +1,29 @@
<script>
import { getAbortSignal } from 'svelte';
let deferred = $state(Promise.withResolvers());
function load(deferred) {
const signal = getAbortSignal();
return new Promise((fulfil, reject) => {
signal.onabort = (e) => {
console.log('aborted');
reject(e.currentTarget.reason);
};
deferred.promise.then(fulfil, reject);
});
}
</script>
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
<button onclick={() => deferred.resolve('hello')}>resolve</button>
<svelte:boundary>
<h1>{await load(deferred)}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -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.

Loading…
Cancel
Save