Skip to content

Commit 666a148

Browse files
committed
implement getAbortSignal
1 parent 8baf164 commit 666a148

File tree

10 files changed

+128
-8
lines changed

10 files changed

+128
-8
lines changed

packages/svelte/src/index-client.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
22
/** @import { EventDispatcher } from './index.js' */
33
/** @import { NotFunction } from './internal/types.js' */
4-
import { untrack } from './internal/client/runtime.js';
4+
import { active_reaction, untrack } from './internal/client/runtime.js';
55
import { is_array } from './internal/shared/utils.js';
66
import { user_effect } from './internal/client/index.js';
77
import * as e from './internal/client/errors.js';
@@ -44,6 +44,14 @@ if (DEV) {
4444
throw_rune_error('$bindable');
4545
}
4646

47+
export function getAbortSignal() {
48+
if (active_reaction === null) {
49+
throw new Error('TODO getAbortSignal can only be called inside a reaction');
50+
}
51+
52+
return (active_reaction.ac ??= new AbortController()).signal;
53+
}
54+
4755
/**
4856
* `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.
4957
* Unlike `$effect`, the provided function only runs once.

packages/svelte/src/index-server.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ export function unmount() {
3535

3636
export async function tick() {}
3737

38+
/** @type {AbortController | null} */
39+
let controller = null;
40+
41+
export function getAbortSignal() {
42+
if (controller === null) {
43+
const c = (controller = new AbortController());
44+
queueMicrotask(() => {
45+
c.abort();
46+
controller = null;
47+
});
48+
}
49+
50+
return controller.signal;
51+
}
52+
3853
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
3954

4055
export { createRawSnippet } from './internal/server/blocks/snippet.js';

packages/svelte/src/internal/client/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ export const EFFECT_ASYNC = 1 << 25;
3030
export const STATE_SYMBOL = Symbol('$state');
3131
export const LEGACY_PROPS = Symbol('legacy props');
3232
export const LOADING_ATTR_SYMBOL = Symbol('');
33+
34+
export const STALE_REACTION = Symbol('stale reaction');

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
EFFECT_ASYNC,
1010
EFFECT_PRESERVED,
1111
MAYBE_DIRTY,
12+
STALE_REACTION,
1213
UNOWNED
1314
} from '#client/constants';
1415
import {
@@ -33,6 +34,7 @@ import { capture, get_pending_boundary } from '../dom/blocks/boundary.js';
3334
import { component_context } from '../context.js';
3435
import { UNINITIALIZED } from '../../../constants.js';
3536
import { current_batch } from './batch.js';
37+
import { noop } from '../../shared/utils.js';
3638

3739
/** @type {Effect | null} */
3840
export let from_async_derived = null;
@@ -77,7 +79,8 @@ export function derived(fn) {
7779
rv: 0,
7880
v: /** @type {V} */ (null),
7981
wv: 0,
80-
parent: parent_derived ?? active_effect
82+
parent: parent_derived ?? active_effect,
83+
ac: null
8184
};
8285

8386
if (DEV && tracing_mode_flag) {
@@ -177,23 +180,35 @@ export function async_derived(fn, location) {
177180
(e) => {
178181
prev = null;
179182

180-
handle_error(e, parent, null, parent.ctx);
183+
if (e === STALE_REACTION) {
184+
if (should_suspend) {
185+
if (!ran) {
186+
boundary.decrement();
187+
} else {
188+
batch.decrement();
189+
}
190+
}
191+
} else {
192+
handle_error(e, parent, null, parent.ctx);
193+
}
181194
}
182195
);
183196
}, EFFECT_ASYNC | EFFECT_PRESERVED);
184197

185198
return new Promise((fulfil) => {
186199
/** @param {Promise<V>} p */
187200
function next(p) {
188-
p.then(() => {
201+
function go() {
189202
if (p === promise) {
190203
fulfil(signal);
191204
} else {
192205
// if the effect re-runs before the initial promise
193206
// resolves, delay resolution until we have a value
194207
next(promise);
195208
}
196-
});
209+
}
210+
211+
p.then(go, go);
197212
}
198213

199214
next(promise);

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ import {
3131
INSPECT_EFFECT,
3232
HEAD_EFFECT,
3333
MAYBE_DIRTY,
34-
EFFECT_PRESERVED
34+
EFFECT_PRESERVED,
35+
STALE_REACTION
3536
} from '#client/constants';
3637
import { set } from './sources.js';
3738
import * as e from '../errors.js';
@@ -112,7 +113,8 @@ function create_effect(type, fn, sync, push = true) {
112113
prev: null,
113114
teardown: null,
114115
transitions: null,
115-
wv: 0
116+
wv: 0,
117+
ac: null
116118
};
117119

118120
if (DEV) {
@@ -425,6 +427,8 @@ export function destroy_effect_children(signal, remove_dom = false) {
425427
signal.first = signal.last = null;
426428

427429
while (effect !== null) {
430+
effect.ac?.abort(STALE_REACTION);
431+
428432
var next = effect.next;
429433

430434
if ((effect.f & ROOT_EFFECT) !== 0) {
@@ -502,6 +506,7 @@ export function destroy_effect(effect, remove_dom = true) {
502506
effect.fn =
503507
effect.nodes_start =
504508
effect.nodes_end =
509+
effect.ac =
505510
null;
506511
}
507512

packages/svelte/src/internal/client/reactivity/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface Reaction extends Signal {
3232
fn: null | Function;
3333
/** Signals that this signal reads from */
3434
deps: null | Value[];
35+
/** An AbortController that aborts when the signal is destroyed */
36+
ac: null | AbortController;
3537
}
3638

3739
export interface Derived<V = unknown> extends Value<V>, Reaction {

packages/svelte/src/internal/client/runtime.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
REACTION_IS_UPDATING,
2727
EFFECT_IS_UPDATING,
2828
EFFECT_ASYNC,
29-
RENDER_EFFECT
29+
RENDER_EFFECT,
30+
STALE_REACTION
3031
} from './constants.js';
3132
import { flush_tasks } from './dom/task.js';
3233
import { internal_set, old_values } from './reactivity/sources.js';
@@ -439,6 +440,11 @@ export function update_reaction(reaction) {
439440

440441
reaction.f |= EFFECT_IS_UPDATING;
441442

443+
if (reaction.ac !== null) {
444+
reaction.ac?.abort(STALE_REACTION);
445+
reaction.ac = null;
446+
}
447+
442448
try {
443449
reaction.f |= REACTION_IS_UPDATING;
444450
var result = /** @type {Function} */ (0, reaction.fn)();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { flushSync, tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, logs, variant }) {
6+
if (variant === 'hydrate') {
7+
await Promise.resolve();
8+
}
9+
10+
const [reset, resolve] = target.querySelectorAll('button');
11+
12+
flushSync(() => reset.click());
13+
await Promise.resolve();
14+
await Promise.resolve();
15+
await Promise.resolve();
16+
await Promise.resolve();
17+
await Promise.resolve();
18+
await Promise.resolve();
19+
await tick();
20+
assert.deepEqual(logs, ['aborted']);
21+
22+
flushSync(() => resolve.click());
23+
await Promise.resolve();
24+
await Promise.resolve();
25+
await Promise.resolve();
26+
await Promise.resolve();
27+
await tick();
28+
assert.htmlEqual(
29+
target.innerHTML,
30+
`
31+
<button>reset</button>
32+
<button>resolve</button>
33+
<h1>hello</h1>
34+
`
35+
);
36+
}
37+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
import { getAbortSignal } from 'svelte';
3+
4+
let deferred = $state(Promise.withResolvers());
5+
6+
function load(deferred) {
7+
const signal = getAbortSignal();
8+
9+
return new Promise((fulfil, reject) => {
10+
signal.onabort = (e) => {
11+
console.log('aborted');
12+
reject(e.currentTarget.reason);
13+
};
14+
15+
deferred.promise.then(fulfil, reject);
16+
});
17+
}
18+
</script>
19+
20+
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
21+
<button onclick={() => deferred.resolve('hello')}>resolve</button>
22+
23+
<svelte:boundary>
24+
<h1>{await load(deferred)}</h1>
25+
26+
{#snippet pending()}
27+
<p>pending</p>
28+
{/snippet}
29+
</svelte:boundary>

packages/svelte/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ declare module 'svelte' {
348348
*/
349349
props: Props;
350350
});
351+
export function getAbortSignal(): AbortSignal;
351352
/**
352353
* `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.
353354
* Unlike `$effect`, the provided function only runs once.

0 commit comments

Comments
 (0)