disallow `flushSync()` inside effects

pull/15844/head
Rich Harris 3 months ago
parent e912f885bd
commit 0430c76da3

@ -74,6 +74,16 @@ Effect cannot be created inside a `$derived` value that was not itself created i
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 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
``` ```
### flush_sync_in_effect
```
Cannot use `flushSync` inside an effect
```
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### hydration_failed ### hydration_failed
``` ```

@ -48,6 +48,14 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> 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 > 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
## flush_sync_in_effect
> Cannot use `flushSync` inside an effect
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## hydration_failed ## hydration_failed
> Failed to hydrate the application > Failed to hydrate the application

@ -335,3 +335,18 @@ export function state_unsafe_mutation() {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);
} }
} }
/**
* Cannot use `flushSync` inside an effect
* @returns {never}
*/
export function flush_sync_in_effect() {
if (DEV) {
const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
}
}

@ -706,6 +706,10 @@ export function process_effects(batch, root) {
* @returns {T} * @returns {T}
*/ */
export function flushSync(fn) { export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) {
e.flush_sync_in_effect();
}
var result; var result;
const batch = Batch.ensure(); const batch = Batch.ensure();

@ -10,6 +10,7 @@ import * as w from '../internal/client/warnings.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { FILENAME } from '../constants.js'; import { FILENAME } from '../constants.js';
import { component_context, dev_current_component_function } from '../internal/client/context.js'; import { component_context, dev_current_component_function } from '../internal/client/context.js';
import { async_mode_flag } from '../internal/flags/index.js';
/** /**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component. * Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
@ -119,8 +120,9 @@ class Svelte4Component {
recover: options.recover recover: options.recover
}); });
// We don't flushSync for custom element wrappers or if the user doesn't want it // We don't flushSync for custom element wrappers or if the user doesn't want it,
if (!options?.props?.$$host || options.sync === false) { // or if we're in async mode since `flushSync()` will fail
if (!async_mode_flag && (!options?.props?.$$host || options.sync === false)) {
flushSync(); flushSync();
} }

@ -6,7 +6,8 @@ import {
effect, effect,
effect_root, effect_root,
render_effect, render_effect,
user_effect user_effect,
user_pre_effect
} from '../../src/internal/client/reactivity/effects'; } from '../../src/internal/client/reactivity/effects';
import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources';
import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import type { Derived, Effect, Source, Value } from '../../src/internal/client/types';
@ -1079,17 +1080,17 @@ describe('signals', () => {
test('nested effects depend on state of upper effects', () => { test('nested effects depend on state of upper effects', () => {
const logs: number[] = []; const logs: number[] = [];
user_effect(() => { user_pre_effect(() => {
const raw = state(0); const raw = state(0);
const proxied = proxy({ current: 0 }); const proxied = proxy({ current: 0 });
// We need those separate, else one working and rerunning the effect // We need those separate, else one working and rerunning the effect
// could mask the other one not rerunning // could mask the other one not rerunning
user_effect(() => { user_pre_effect(() => {
logs.push($.get(raw)); logs.push($.get(raw));
}); });
user_effect(() => { user_pre_effect(() => {
logs.push(proxied.current); logs.push(proxied.current);
}); });
@ -1097,7 +1098,7 @@ describe('signals', () => {
// together with the reading effects // together with the reading effects
flushSync(); flushSync();
user_effect(() => { user_pre_effect(() => {
$.untrack(() => { $.untrack(() => {
set(raw, $.get(raw) + 1); set(raw, $.get(raw) + 1);
proxied.current += 1; proxied.current += 1;

Loading…
Cancel
Save