Skip to content

Commit aff0f50

Browse files
committed
merge main
2 parents 1a6cd7d + b673145 commit aff0f50

File tree

13 files changed

+164
-49
lines changed

13 files changed

+164
-49
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
@@ -84,6 +84,12 @@ The `flushSync()` function can be used to flush any pending effects synchronousl
8484

8585
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
8686

87+
### get_abort_signal_outside_reaction
88+
89+
```
90+
`getAbortSignal()` can only be called inside an effect or derived
91+
```
92+
8793
### hydration_failed
8894

8995
```

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ The `flushSync()` function can be used to flush any pending effects synchronousl
5656

5757
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
5858

59+
## get_abort_signal_outside_reaction
60+
61+
> `getAbortSignal()` can only be called inside an effect or derived
62+
5963
## hydration_failed
6064

6165
> Failed to hydrate the application

packages/svelte/src/index-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ if (DEV) {
6969
*/
7070
export function getAbortSignal() {
7171
if (active_reaction === null) {
72-
throw new Error('TODO getAbortSignal can only be called inside a reaction');
72+
e.get_abort_signal_outside_reaction();
7373
}
7474

7575
return (active_reaction.ac ??= new AbortController()).signal;

packages/svelte/src/index-server.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,7 @@ export async function tick() {}
3737

3838
export async function settled() {}
3939

40-
/** @type {AbortController | null} */
41-
let controller = null;
42-
43-
export function getAbortSignal() {
44-
if (controller === null) {
45-
const c = (controller = new AbortController());
46-
queueMicrotask(() => {
47-
c.abort();
48-
controller = null;
49-
});
50-
}
51-
52-
return controller.signal;
53-
}
40+
export { getAbortSignal } from './internal/server/abort-signal.js';
5441

5542
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
5643

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ export const PROXY_PATH_SYMBOL = Symbol('proxy path');
3434
export const LEGACY_PROPS = Symbol('legacy props');
3535
export const LOADING_ATTR_SYMBOL = Symbol('');
3636

37-
export const STALE_REACTION = Symbol('stale reaction');
37+
// allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError`
38+
export const STALE_REACTION = new (class StaleReactionError extends Error {
39+
name = 'StaleReactionError';
40+
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
41+
})();
3842

3943
export const ELEMENT_NODE = 1;
4044
export const TEXT_NODE = 3;

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,29 @@ export function flush_sync_in_effect() {
204204
const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
205205

206206
error.name = 'Svelte error';
207+
207208
throw error;
208209
} else {
209210
throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
210211
}
211212
}
212213

214+
/**
215+
* `getAbortSignal()` can only be called inside an effect or derived
216+
* @returns {never}
217+
*/
218+
export function get_abort_signal_outside_reaction() {
219+
if (DEV) {
220+
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`);
221+
222+
error.name = 'Svelte error';
223+
224+
throw error;
225+
} else {
226+
throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`);
227+
}
228+
}
229+
213230
/**
214231
* Failed to hydrate the application
215232
* @returns {never}
@@ -319,6 +336,7 @@ export function set_context_after_init() {
319336
const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`);
320337

321338
error.name = 'Svelte error';
339+
322340
throw error;
323341
} else {
324342
throw new Error(`https://svelte.dev/e/set_context_after_init`);

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ export function update_reaction(reaction) {
306306
reaction.ac = null;
307307
}
308308

309+
if (reaction.ac !== null) {
310+
reaction.ac.abort(STALE_REACTION);
311+
reaction.ac = null;
312+
}
313+
309314
try {
310315
reaction.f |= REACTION_IS_UPDATING;
311316
var result = /** @type {Function} */ (0, reaction.fn)();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { STALE_REACTION } from '#client/constants';
2+
3+
/** @type {AbortController | null} */
4+
export let controller = null;
5+
6+
export function abort() {
7+
controller?.abort(STALE_REACTION);
8+
controller = null;
9+
}
10+
11+
export function getAbortSignal() {
12+
return (controller ??= new AbortController()).signal;
13+
}

packages/svelte/src/internal/server/index.js

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { validate_store } from '../shared/validate.js';
1818
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
1919
import { reset_elements } from './dev.js';
2020
import { Payload } from './payload.js';
21+
import { abort } from './abort-signal.js';
2122

2223
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
2324
// https://infra.spec.whatwg.org/#noncharacter
@@ -66,50 +67,54 @@ export let on_destroy = [];
6667
* @returns {RenderOutput}
6768
*/
6869
export function render(component, options = {}) {
69-
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
70+
try {
71+
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
7072

71-
const prev_on_destroy = on_destroy;
72-
on_destroy = [];
73-
payload.out += BLOCK_OPEN;
73+
const prev_on_destroy = on_destroy;
74+
on_destroy = [];
75+
payload.out += BLOCK_OPEN;
7476

75-
let reset_reset_element;
77+
let reset_reset_element;
7678

77-
if (DEV) {
78-
// prevent parent/child element state being corrupted by a bad render
79-
reset_reset_element = reset_elements();
80-
}
79+
if (DEV) {
80+
// prevent parent/child element state being corrupted by a bad render
81+
reset_reset_element = reset_elements();
82+
}
8183

82-
if (options.context) {
83-
push();
84-
/** @type {Component} */ (current_component).c = options.context;
85-
}
84+
if (options.context) {
85+
push();
86+
/** @type {Component} */ (current_component).c = options.context;
87+
}
8688

87-
// @ts-expect-error
88-
component(payload, options.props ?? {}, {}, {});
89+
// @ts-expect-error
90+
component(payload, options.props ?? {}, {}, {});
8991

90-
if (options.context) {
91-
pop();
92-
}
92+
if (options.context) {
93+
pop();
94+
}
9395

94-
if (reset_reset_element) {
95-
reset_reset_element();
96-
}
96+
if (reset_reset_element) {
97+
reset_reset_element();
98+
}
9799

98-
payload.out += BLOCK_CLOSE;
99-
for (const cleanup of on_destroy) cleanup();
100-
on_destroy = prev_on_destroy;
100+
payload.out += BLOCK_CLOSE;
101+
for (const cleanup of on_destroy) cleanup();
102+
on_destroy = prev_on_destroy;
101103

102-
let head = payload.head.out + payload.head.title;
104+
let head = payload.head.out + payload.head.title;
103105

104-
for (const { hash, code } of payload.css) {
105-
head += `<style id="${hash}">${code}</style>`;
106-
}
106+
for (const { hash, code } of payload.css) {
107+
head += `<style id="${hash}">${code}</style>`;
108+
}
107109

108-
return {
109-
head,
110-
html: payload.out,
111-
body: payload.out
112-
};
110+
return {
111+
head,
112+
html: payload.out,
113+
body: payload.out
114+
};
115+
} finally {
116+
abort();
117+
}
113118
}
114119

115120
/**

0 commit comments

Comments
 (0)