fix: improve infinite loop capturing (#9721)

pull/9707/head
Dominic Gannaway 12 months ago committed by GitHub
parent 8266229c42
commit 7eba35b9d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve infinite loop capturing

@ -54,6 +54,8 @@ export let current_effect = null;
/** @type {null | import('./types.js').Signal[]} */ /** @type {null | import('./types.js').Signal[]} */
let current_dependencies = null; let current_dependencies = null;
let current_dependencies_index = 0; let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null;
// Handling capturing of signals from object property getters // Handling capturing of signals from object property getters
let current_should_capture_signal = false; let current_should_capture_signal = false;
/** If `true`, `get`ting the signal should not register it as a dependency */ /** If `true`, `get`ting the signal should not register it as a dependency */
@ -282,6 +284,7 @@ function execute_signal_fn(signal) {
const init = signal.i; const init = signal.i;
const previous_dependencies = current_dependencies; const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index; const previous_dependencies_index = current_dependencies_index;
const previous_untracked_writes = current_untracked_writes;
const previous_consumer = current_consumer; const previous_consumer = current_consumer;
const previous_block = current_block; const previous_block = current_block;
const previous_component_context = current_component_context; const previous_component_context = current_component_context;
@ -290,6 +293,7 @@ function execute_signal_fn(signal) {
const previous_untracking = current_untracking; const previous_untracking = current_untracking;
current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null); current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null);
current_dependencies_index = 0; current_dependencies_index = 0;
current_untracked_writes = null;
current_consumer = signal; current_consumer = signal;
current_block = signal.b; current_block = signal.b;
current_component_context = signal.x; current_component_context = signal.x;
@ -347,6 +351,7 @@ function execute_signal_fn(signal) {
} finally { } finally {
current_dependencies = previous_dependencies; current_dependencies = previous_dependencies;
current_dependencies_index = previous_dependencies_index; current_dependencies_index = previous_dependencies_index;
current_untracked_writes = previous_untracked_writes;
current_consumer = previous_consumer; current_consumer = previous_consumer;
current_block = previous_block; current_block = previous_block;
current_component_context = previous_component_context; current_component_context = previous_component_context;
@ -469,6 +474,19 @@ export function execute_effect(signal) {
} }
} }
function infinite_loop_guard() {
if (flush_count > 100) {
throw new Error(
'ERR_SVELTE_TOO_MANY_UPDATES' +
(DEV
? ': 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_count++;
}
/** /**
* @param {Array<import('./types.js').EffectSignal>} effects * @param {Array<import('./types.js').EffectSignal>} effects
* @returns {void} * @returns {void}
@ -476,16 +494,7 @@ export function execute_effect(signal) {
function flush_queued_effects(effects) { function flush_queued_effects(effects) {
const length = effects.length; const length = effects.length;
if (length > 0) { if (length > 0) {
if (flush_count > 100) { infinite_loop_guard();
throw new Error(
'ERR_SVELTE_TOO_MANY_UPDATES' +
(DEV
? ': 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_count++;
let i; let i;
for (i = 0; i < length; i++) { for (i = 0; i < length; i++) {
const signal = effects[i]; const signal = effects[i];
@ -606,13 +615,13 @@ export function flushSync(fn) {
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects; const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects; const previous_queued_effects = current_queued_effects;
try { try {
infinite_loop_guard();
/** @type {import('./types.js').EffectSignal[]} */ /** @type {import('./types.js').EffectSignal[]} */
const pre_and_render_effects = []; const pre_and_render_effects = [];
/** @type {import('./types.js').EffectSignal[]} */ /** @type {import('./types.js').EffectSignal[]} */
const effects = []; const effects = [];
current_scheduler_mode = FLUSH_SYNC; current_scheduler_mode = FLUSH_SYNC;
flush_count = 0;
current_queued_pre_and_render_effects = pre_and_render_effects; current_queued_pre_and_render_effects = pre_and_render_effects;
current_queued_effects = effects; current_queued_effects = effects;
flush_queued_effects(previous_queued_pre_and_render_effects); flush_queued_effects(previous_queued_pre_and_render_effects);
@ -626,6 +635,7 @@ export function flushSync(fn) {
if (is_task_queued) { if (is_task_queued) {
process_task(); process_task();
} }
flush_count = 0;
} finally { } finally {
current_scheduler_mode = previous_scheduler_mode; current_scheduler_mode = previous_scheduler_mode;
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects; current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
@ -814,6 +824,15 @@ export function get(signal) {
} else if (signal !== current_dependencies[current_dependencies.length - 1]) { } else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal); current_dependencies.push(signal);
} }
if (
current_untracked_writes !== null &&
current_effect !== null &&
(current_effect.f & CLEAN) !== 0 &&
current_untracked_writes.includes(signal)
) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
}
} }
if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) { if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) {
@ -1024,12 +1043,18 @@ export function set_signal_value(signal, value) {
is_runes(component_context) && is_runes(component_context) &&
current_effect !== null && current_effect !== null &&
current_effect.c === null && current_effect.c === null &&
(current_effect.f & CLEAN) !== 0 && (current_effect.f & CLEAN) !== 0
current_dependencies !== null &&
current_dependencies.includes(signal)
) { ) {
set_signal_status(current_effect, DIRTY); if (current_dependencies !== null && current_dependencies.includes(signal)) {
schedule_effect(current_effect, false); set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
} else {
if (current_untracked_writes === null) {
current_untracked_writes = [signal];
} else {
current_untracked_writes.push(signal);
}
}
} }
mark_signal_consumers(signal, DIRTY, true); mark_signal_consumers(signal, DIRTY, true);
// If we have afterUpdates locally on the component, but we're within a render effect // If we have afterUpdates locally on the component, but we're within a render effect

@ -63,6 +63,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
intro?: boolean; intro?: boolean;
load_compiled?: boolean; load_compiled?: boolean;
error?: string; error?: string;
runtime_error?: string;
warnings?: string[]; warnings?: string[];
expect_unhandled_rejections?: boolean; expect_unhandled_rejections?: boolean;
withoutNormalizeHtml?: boolean; withoutNormalizeHtml?: boolean;
@ -315,7 +316,9 @@ async function run_test_variant(
} }
} }
} catch (err) { } catch (err) {
if (config.error && !unintended_error) { if (config.runtime_error) {
assert.equal((err as Error).message, config.runtime_error);
} else if (config.error && !unintended_error) {
assert.equal((err as Error).message, config.error); assert.equal((err as Error).message, config.error);
} else { } else {
throw err; throw err;

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
runtime_error:
'ERR_SVELTE_TOO_MANY_UPDATES: 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.',
async test({ assert, target }) {}
});

@ -0,0 +1,11 @@
<script>
const v = { value: 1 };
let s = $state(v)
$effect(() => {
s = v;
s;
});
</script>
{JSON.stringify(s)}
Loading…
Cancel
Save