Skip to content

Commit 7eba35b

Browse files
authored
fix: improve infinite loop capturing (#9721)
1 parent 8266229 commit 7eba35b

File tree

5 files changed

+68
-17
lines changed

5 files changed

+68
-17
lines changed

.changeset/tall-tigers-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: improve infinite loop capturing

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

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export let current_effect = null;
5454
/** @type {null | import('./types.js').Signal[]} */
5555
let current_dependencies = null;
5656
let current_dependencies_index = 0;
57+
/** @type {null | import('./types.js').Signal[]} */
58+
let current_untracked_writes = null;
5759
// Handling capturing of signals from object property getters
5860
let current_should_capture_signal = false;
5961
/** If `true`, `get`ting the signal should not register it as a dependency */
@@ -282,6 +284,7 @@ function execute_signal_fn(signal) {
282284
const init = signal.i;
283285
const previous_dependencies = current_dependencies;
284286
const previous_dependencies_index = current_dependencies_index;
287+
const previous_untracked_writes = current_untracked_writes;
285288
const previous_consumer = current_consumer;
286289
const previous_block = current_block;
287290
const previous_component_context = current_component_context;
@@ -290,6 +293,7 @@ function execute_signal_fn(signal) {
290293
const previous_untracking = current_untracking;
291294
current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null);
292295
current_dependencies_index = 0;
296+
current_untracked_writes = null;
293297
current_consumer = signal;
294298
current_block = signal.b;
295299
current_component_context = signal.x;
@@ -347,6 +351,7 @@ function execute_signal_fn(signal) {
347351
} finally {
348352
current_dependencies = previous_dependencies;
349353
current_dependencies_index = previous_dependencies_index;
354+
current_untracked_writes = previous_untracked_writes;
350355
current_consumer = previous_consumer;
351356
current_block = previous_block;
352357
current_component_context = previous_component_context;
@@ -469,23 +474,27 @@ export function execute_effect(signal) {
469474
}
470475
}
471476

477+
function infinite_loop_guard() {
478+
if (flush_count > 100) {
479+
throw new Error(
480+
'ERR_SVELTE_TOO_MANY_UPDATES' +
481+
(DEV
482+
? ': Maximum update depth exceeded. This can happen when a reactive block or effect ' +
483+
'repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops.'
484+
: '')
485+
);
486+
}
487+
flush_count++;
488+
}
489+
472490
/**
473491
* @param {Array<import('./types.js').EffectSignal>} effects
474492
* @returns {void}
475493
*/
476494
function flush_queued_effects(effects) {
477495
const length = effects.length;
478496
if (length > 0) {
479-
if (flush_count > 100) {
480-
throw new Error(
481-
'ERR_SVELTE_TOO_MANY_UPDATES' +
482-
(DEV
483-
? ': Maximum update depth exceeded. This can happen when a reactive block or effect ' +
484-
'repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops.'
485-
: '')
486-
);
487-
}
488-
flush_count++;
497+
infinite_loop_guard();
489498
let i;
490499
for (i = 0; i < length; i++) {
491500
const signal = effects[i];
@@ -606,13 +615,13 @@ export function flushSync(fn) {
606615
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
607616
const previous_queued_effects = current_queued_effects;
608617
try {
618+
infinite_loop_guard();
609619
/** @type {import('./types.js').EffectSignal[]} */
610620
const pre_and_render_effects = [];
611621

612622
/** @type {import('./types.js').EffectSignal[]} */
613623
const effects = [];
614624
current_scheduler_mode = FLUSH_SYNC;
615-
flush_count = 0;
616625
current_queued_pre_and_render_effects = pre_and_render_effects;
617626
current_queued_effects = effects;
618627
flush_queued_effects(previous_queued_pre_and_render_effects);
@@ -626,6 +635,7 @@ export function flushSync(fn) {
626635
if (is_task_queued) {
627636
process_task();
628637
}
638+
flush_count = 0;
629639
} finally {
630640
current_scheduler_mode = previous_scheduler_mode;
631641
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
@@ -814,6 +824,15 @@ export function get(signal) {
814824
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
815825
current_dependencies.push(signal);
816826
}
827+
if (
828+
current_untracked_writes !== null &&
829+
current_effect !== null &&
830+
(current_effect.f & CLEAN) !== 0 &&
831+
current_untracked_writes.includes(signal)
832+
) {
833+
set_signal_status(current_effect, DIRTY);
834+
schedule_effect(current_effect, false);
835+
}
817836
}
818837

819838
if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) {
@@ -1024,12 +1043,18 @@ export function set_signal_value(signal, value) {
10241043
is_runes(component_context) &&
10251044
current_effect !== null &&
10261045
current_effect.c === null &&
1027-
(current_effect.f & CLEAN) !== 0 &&
1028-
current_dependencies !== null &&
1029-
current_dependencies.includes(signal)
1046+
(current_effect.f & CLEAN) !== 0
10301047
) {
1031-
set_signal_status(current_effect, DIRTY);
1032-
schedule_effect(current_effect, false);
1048+
if (current_dependencies !== null && current_dependencies.includes(signal)) {
1049+
set_signal_status(current_effect, DIRTY);
1050+
schedule_effect(current_effect, false);
1051+
} else {
1052+
if (current_untracked_writes === null) {
1053+
current_untracked_writes = [signal];
1054+
} else {
1055+
current_untracked_writes.push(signal);
1056+
}
1057+
}
10331058
}
10341059
mark_signal_consumers(signal, DIRTY, true);
10351060
// If we have afterUpdates locally on the component, but we're within a render effect

packages/svelte/tests/runtime-legacy/shared.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
6363
intro?: boolean;
6464
load_compiled?: boolean;
6565
error?: string;
66+
runtime_error?: string;
6667
warnings?: string[];
6768
expect_unhandled_rejections?: boolean;
6869
withoutNormalizeHtml?: boolean;
@@ -315,7 +316,9 @@ async function run_test_variant(
315316
}
316317
}
317318
} catch (err) {
318-
if (config.error && !unintended_error) {
319+
if (config.runtime_error) {
320+
assert.equal((err as Error).message, config.runtime_error);
321+
} else if (config.error && !unintended_error) {
319322
assert.equal((err as Error).message, config.error);
320323
} else {
321324
throw err;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
runtime_error:
5+
'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.',
6+
async test({ assert, target }) {}
7+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
const v = { value: 1 };
3+
let s = $state(v)
4+
5+
$effect(() => {
6+
s = v;
7+
s;
8+
});
9+
</script>
10+
11+
{JSON.stringify(s)}

0 commit comments

Comments
 (0)