Skip to content

fix: improve infinite loop capturing #9721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-tigers-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: improve infinite loop capturing
57 changes: 41 additions & 16 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export let current_effect = null;
/** @type {null | import('./types.js').Signal[]} */
let current_dependencies = null;
let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null;
// Handling capturing of signals from object property getters
let current_should_capture_signal = false;
/** If `true`, `get`ting the signal should not register it as a dependency */
Expand Down Expand Up @@ -282,6 +284,7 @@ function execute_signal_fn(signal) {
const init = signal.i;
const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index;
const previous_untracked_writes = current_untracked_writes;
const previous_consumer = current_consumer;
const previous_block = current_block;
const previous_component_context = current_component_context;
Expand All @@ -290,6 +293,7 @@ function execute_signal_fn(signal) {
const previous_untracking = current_untracking;
current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null);
current_dependencies_index = 0;
current_untracked_writes = null;
current_consumer = signal;
current_block = signal.b;
current_component_context = signal.x;
Expand Down Expand Up @@ -347,6 +351,7 @@ function execute_signal_fn(signal) {
} finally {
current_dependencies = previous_dependencies;
current_dependencies_index = previous_dependencies_index;
current_untracked_writes = previous_untracked_writes;
current_consumer = previous_consumer;
current_block = previous_block;
current_component_context = previous_component_context;
Expand Down Expand Up @@ -469,23 +474,27 @@ 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
* @returns {void}
*/
function flush_queued_effects(effects) {
const length = effects.length;
if (length > 0) {
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++;
infinite_loop_guard();
let i;
for (i = 0; i < length; i++) {
const signal = effects[i];
Expand Down Expand Up @@ -606,13 +615,13 @@ export function flushSync(fn) {
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects;
try {
infinite_loop_guard();
/** @type {import('./types.js').EffectSignal[]} */
const pre_and_render_effects = [];

/** @type {import('./types.js').EffectSignal[]} */
const effects = [];
current_scheduler_mode = FLUSH_SYNC;
flush_count = 0;
current_queued_pre_and_render_effects = pre_and_render_effects;
current_queued_effects = effects;
flush_queued_effects(previous_queued_pre_and_render_effects);
Expand All @@ -626,6 +635,7 @@ export function flushSync(fn) {
if (is_task_queued) {
process_task();
}
flush_count = 0;
} finally {
current_scheduler_mode = previous_scheduler_mode;
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
Expand Down Expand Up @@ -814,6 +824,15 @@ export function get(signal) {
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
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)) {
Expand Down Expand Up @@ -1024,12 +1043,18 @@ export function set_signal_value(signal, value) {
is_runes(component_context) &&
current_effect !== null &&
current_effect.c === null &&
(current_effect.f & CLEAN) !== 0 &&
current_dependencies !== null &&
current_dependencies.includes(signal)
(current_effect.f & CLEAN) !== 0
) {
set_signal_status(current_effect, DIRTY);
schedule_effect(current_effect, false);
if (current_dependencies !== null && current_dependencies.includes(signal)) {
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);
// If we have afterUpdates locally on the component, but we're within a render effect
Expand Down
5 changes: 4 additions & 1 deletion packages/svelte/tests/runtime-legacy/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
intro?: boolean;
load_compiled?: boolean;
error?: string;
runtime_error?: string;
warnings?: string[];
expect_unhandled_rejections?: boolean;
withoutNormalizeHtml?: boolean;
Expand Down Expand Up @@ -315,7 +316,9 @@ async function run_test_variant(
}
}
} 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);
} else {
throw err;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) {}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
const v = { value: 1 };
let s = $state(v)

$effect(() => {
s = v;
s;
});
</script>

{JSON.stringify(s)}