feat: add infinite loop effect callstack (#13231)

This adds a new dev-time only `dev_effect_stack` variable, which executed effects are pushed to and eventually cleared out after everything's settled. If it doesn't settle however, i.e. you run into an infinite loop, the last ten effects are printed out so you get an idea where the error is coming from.
For proper source mapping I also had add location info to the generated effects.
Closes #13192
pull/13228/head
Simon H 3 months ago committed by GitHub
parent e5c840c87b
commit a6df4ebfcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add infinite loop effect callstack

@ -15,7 +15,10 @@ export function ExpressionStatement(node, context) {
const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0]));
return b.stmt(b.call(callee, /** @type {Expression} */ (func)));
const expr = b.call(callee, /** @type {Expression} */ (func));
expr.callee.loc = node.expression.callee.loc; // ensure correct mapping
return b.stmt(expr);
}
}

@ -62,6 +62,8 @@ export function set_is_destroying_effect(value) {
let current_queued_root_effects = [];
let flush_count = 0;
/** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions
/** @type {null | Reaction} */
@ -443,8 +445,11 @@ export function update_effect(effect) {
execute_effect_teardown(effect);
var teardown = update_reaction(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.version = current_version;
if (DEV) {
dev_effect_stack.push(effect);
}
} catch (error) {
handle_error(/** @type {Error} */ (error), effect, current_component_context);
} finally {
@ -460,7 +465,25 @@ export function update_effect(effect) {
function infinite_loop_guard() {
if (flush_count > 1000) {
flush_count = 0;
e.effect_update_depth_exceeded();
if (DEV) {
try {
e.effect_update_depth_exceeded();
} catch (error) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
// eslint-disable-next-line no-console
console.error(
'Last ten effects were: ',
dev_effect_stack.slice(-10).map((d) => d.fn)
);
dev_effect_stack = [];
throw error;
}
} else {
e.effect_update_depth_exceeded();
}
}
flush_count++;
}
@ -541,6 +564,9 @@ function process_deferred() {
flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) {
flush_count = 0;
if (DEV) {
dev_effect_stack = [];
}
}
}
@ -682,6 +708,9 @@ export function flush_sync(fn) {
}
flush_count = 0;
if (DEV) {
dev_effect_stack = [];
}
return result;
} finally {

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
client: [
{ str: '$effect.pre', strGenerated: '$.user_pre_effect' },
{ str: '$effect', strGenerated: '$.user_effect' }
],
server: []
});

@ -0,0 +1,9 @@
<script lang="ts">
$effect(() => {
foo;
});
$effect.pre(() => {
bar;
});
</script>
Loading…
Cancel
Save