From 2b6a8c91f1d9ac4f28d85413e22d97e946a5dc4c Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 30 Apr 2026 19:47:22 +0700 Subject: [PATCH] fix(master): break effect_update_depth_exceeded loop in countdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two $effects were reading state they also wrote, turning each into its own dependency: - AutoCountdown reset effect: `now = tickStart` read tickStart after writing it. Use a local snapshot so neither rune is read post-write. - MasterPanel auto-call effect: `tickCount++` read tickCount. Drop the bump here — AutoCountdown's reset effect already re-baselines on the rising edge of `running` and on `duration` change, so re-arms remain covered. handleDrawNext keeps its per-draw bump (not in effect scope). --- web/src/lib/AutoCountdown.svelte | 7 +++++-- web/src/lib/MasterPanel.svelte | 10 ++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/web/src/lib/AutoCountdown.svelte b/web/src/lib/AutoCountdown.svelte index b13a4d1..9d090ae 100644 --- a/web/src/lib/AutoCountdown.svelte +++ b/web/src/lib/AutoCountdown.svelte @@ -24,12 +24,15 @@ // Re-baseline whenever the parent bumps tickKey, running flips on, or // duration changes mid-run (e.g. host moves the speed slider). + // Use a local snapshot so we don't read `tickStart` after writing it — + // that would make this effect depend on its own write and infinite-loop. $effect(() => { tickKey; // subscribe duration; // subscribe — keeps the contract explicit, not parent-coupled if (running) { - tickStart = performance.now(); - now = tickStart; + const t = performance.now(); + tickStart = t; + now = t; } }); diff --git a/web/src/lib/MasterPanel.svelte b/web/src/lib/MasterPanel.svelte index 6cd463f..eb4cf3a 100644 --- a/web/src/lib/MasterPanel.svelte +++ b/web/src/lib/MasterPanel.svelte @@ -123,12 +123,10 @@ autoRunning = false; return; } - // Re-baseline countdown on every (re-)arm: rising edge of autoRunning, - // speed change, autoCallEnabled toggle. handleDrawNext bumps it again - // per tick so the ring re-fills before the next interval elapses. - // Safe self-write: this effect doesn't read tickCount, so the bump - // can't re-trigger it. - tickCount++; + // No tickCount bump here on purpose: `tickCount++` would read tickCount + // and turn this effect into its own dependency (effect_update_depth_exceeded). + // AutoCountdown's reset effect already re-baselines on `running` rising + // edge and on `duration` change, so re-arms are covered without our help. const ms = settings.autoCallSpeed * 1000; const id = setInterval(() => { if (!state || state.remaining.length === 0) {