Skip to content

Commit b673145

Browse files
feat: add getAbortSignal() (#16266)
* WIP getAbortSignal * add test * regenerate * add error code * changeset * regenerate * try this * { stale: true } * fix test * lint * abort synchronously in SSR * make STALE_REACTION a `StaleReactionError extends Error` * make non-optional * Update packages/svelte/src/internal/server/abort-signal.js Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com> --------- Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com>
1 parent 7c8be60 commit b673145

File tree

16 files changed

+231
-38
lines changed

16 files changed

+231
-38
lines changed

.changeset/short-fireants-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add `getAbortSignal()`

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ Effect cannot be created inside a `$derived` value that was not itself created i
7474
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
7575
```
7676

77+
### get_abort_signal_outside_reaction
78+
79+
```
80+
`getAbortSignal()` can only be called inside an effect or derived
81+
```
82+
7783
### hydration_failed
7884

7985
```

packages/svelte/messages/client-errors/errors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
4848

4949
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
5050
51+
## get_abort_signal_outside_reaction
52+
53+
> `getAbortSignal()` can only be called inside an effect or derived
54+
5155
## hydration_failed
5256

5357
> Failed to hydrate the application

packages/svelte/src/index-client.js

Lines changed: 32 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,37 @@ if (DEV) {
4444
throw_rune_error('$bindable');
4545
}
4646

47+
/**
48+
* 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.
49+
*
50+
* Must be called while a derived or effect is running.
51+
*
52+
* ```svelte
53+
* <script>
54+
* import { getAbortSignal } from 'svelte';
55+
*
56+
* let { id } = $props();
57+
*
58+
* async function getData(id) {
59+
* const response = await fetch(`/items/${id}`, {
60+
* signal: getAbortSignal()
61+
* });
62+
*
63+
* return await response.json();
64+
* }
65+
*
66+
* const data = $derived(await getData(id));
67+
* </script>
68+
* ```
69+
*/
70+
export function getAbortSignal() {
71+
if (active_reaction === null) {
72+
e.get_abort_signal_outside_reaction();
73+
}
74+
75+
return (active_reaction.ac ??= new AbortController()).signal;
76+
}
77+
4778
/**
4879
* `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.
4980
* Unlike `$effect`, the provided function only runs once.

packages/svelte/src/index-server.js

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

3636
export async function tick() {}
3737

38+
export { getAbortSignal } from './internal/server/abort-signal.js';
39+
3840
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
3941

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export const LEGACY_PROPS = Symbol('legacy props');
2727
export const LOADING_ATTR_SYMBOL = Symbol('');
2828
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
2929

30+
// allow users to ignore aborted signal errors if `reason.stale`
31+
export const STALE_REACTION = new (class StaleReactionError extends Error {
32+
name = 'StaleReactionError';
33+
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
34+
})();
35+
3036
export const ELEMENT_NODE = 1;
3137
export const TEXT_NODE = 3;
3238
export const COMMENT_NODE = 8;

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ export function effect_update_depth_exceeded() {
195195
}
196196
}
197197

198+
/**
199+
* `getAbortSignal()` can only be called inside an effect or derived
200+
* @returns {never}
201+
*/
202+
export function get_abort_signal_outside_reaction() {
203+
if (DEV) {
204+
const error = new Error(`get_abort_signal_outside_reaction\n\`getAbortSignal()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/get_abort_signal_outside_reaction`);
205+
206+
error.name = 'Svelte error';
207+
208+
throw error;
209+
} else {
210+
throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`);
211+
}
212+
}
213+
198214
/**
199215
* Failed to hydrate the application
200216
* @returns {never}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export function derived(fn) {
5353
rv: 0,
5454
v: /** @type {V} */ (null),
5555
wv: 0,
56-
parent: parent_derived ?? active_effect
56+
parent: parent_derived ?? active_effect,
57+
ac: null
5758
};
5859

5960
if (DEV && tracing_mode_flag) {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
HEAD_EFFECT,
3333
MAYBE_DIRTY,
3434
EFFECT_HAS_DERIVED,
35-
BOUNDARY_EFFECT
35+
BOUNDARY_EFFECT,
36+
STALE_REACTION
3637
} from '#client/constants';
3738
import { set } from './sources.js';
3839
import * as e from '../errors.js';
@@ -106,7 +107,8 @@ function create_effect(type, fn, sync, push = true) {
106107
prev: null,
107108
teardown: null,
108109
transitions: null,
109-
wv: 0
110+
wv: 0,
111+
ac: null
110112
};
111113

112114
if (DEV) {
@@ -397,6 +399,8 @@ export function destroy_effect_children(signal, remove_dom = false) {
397399
signal.first = signal.last = null;
398400

399401
while (effect !== null) {
402+
effect.ac?.abort(STALE_REACTION);
403+
400404
var next = effect.next;
401405

402406
if ((effect.f & ROOT_EFFECT) !== 0) {
@@ -478,6 +482,7 @@ export function destroy_effect(effect, remove_dom = true) {
478482
effect.fn =
479483
effect.nodes_start =
480484
effect.nodes_end =
485+
effect.ac =
481486
null;
482487
}
483488

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface Reaction extends Signal {
4040
fn: null | Function;
4141
/** Signals that this signal reads from */
4242
deps: null | Value[];
43+
/** An AbortController that aborts when the signal is destroyed */
44+
ac: null | AbortController;
4345
}
4446

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

0 commit comments

Comments
 (0)