From 20e4d2cdec048503ae78c3fd00f4e3c13aebfad9 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Tue, 24 Mar 2026 13:03:48 +0100 Subject: [PATCH] fix(electrobun): eliminate $effect/$derived cycles in 3 more components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TaskBoardTab: $effect called loadTasks() which wrote ++pollToken ($state) → triggered $effect re-run → infinite loop. Fix: onMount instead. Also tasksByCol $derived created new objects via .reduce/.filter. FileBrowser: $effect read openDirs (via new Set(openDirs)) AND wrote to it (openDirs = s) → infinite loop. Fix: onMount with fresh Set. CommsTab: $effect called loadChannels()/loadAgents() which wrote $state → potential cycle. Fix: onMount instead. Rule: NEVER use $effect for initialization that writes to $state. Always use onMount for async init + side effects. --- ui-electrobun/src/mainview/CommsTab.svelte | 6 +++--- ui-electrobun/src/mainview/FileBrowser.svelte | 10 +++++----- .../src/mainview/TaskBoardTab.svelte | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/ui-electrobun/src/mainview/CommsTab.svelte b/ui-electrobun/src/mainview/CommsTab.svelte index 2e676f7..069437f 100644 --- a/ui-electrobun/src/mainview/CommsTab.svelte +++ b/ui-electrobun/src/mainview/CommsTab.svelte @@ -186,11 +186,12 @@ } } - $effect(() => { + // Use onMount instead of $effect — loadChannels/loadAgents write to $state + import { onMount } from 'svelte'; + onMount(() => { loadChannels(); loadAgents(); appRpc.addMessageListener('btmsg.newMessage', onNewMessage); - // Feature 4: Fallback 30s poll for missed events pollTimer = setInterval(() => { if (mode === 'channels' && activeChannelId) { loadChannelMessages(activeChannelId); @@ -198,7 +199,6 @@ loadDmMessages(activeDmAgentId); } }, 30000); - return () => { if (pollTimer) clearInterval(pollTimer); appRpc.removeMessageListener?.('btmsg.newMessage', onNewMessage); diff --git a/ui-electrobun/src/mainview/FileBrowser.svelte b/ui-electrobun/src/mainview/FileBrowser.svelte index 6a6452e..cc0c2d1 100644 --- a/ui-electrobun/src/mainview/FileBrowser.svelte +++ b/ui-electrobun/src/mainview/FileBrowser.svelte @@ -263,13 +263,13 @@ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } - // Load root directory on mount - $effect(() => { + // Load root directory on mount — use onMount, NOT $effect + // $effect reads openDirs (via new Set(openDirs)) and writes to it → infinite loop + import { onMount } from 'svelte'; + onMount(() => { if (cwd) { loadDir(cwd); - const s = new Set(openDirs); - s.add(cwd); - openDirs = s; + openDirs = new Set([cwd]); } }); diff --git a/ui-electrobun/src/mainview/TaskBoardTab.svelte b/ui-electrobun/src/mainview/TaskBoardTab.svelte index 5ab7b99..97248c1 100644 --- a/ui-electrobun/src/mainview/TaskBoardTab.svelte +++ b/ui-electrobun/src/mainview/TaskBoardTab.svelte @@ -51,12 +51,13 @@ // ── Derived ────────────────────────────────────────────────────────── - let tasksByCol = $derived( - COLUMNS.reduce((acc, col) => { + // NO $derived — .reduce creates new objects every evaluation → infinite loop + function getTasksByCol(): Record { + return COLUMNS.reduce((acc, col) => { acc[col] = tasks.filter(t => t.status === col); return acc; - }, {} as Record) - ); + }, {} as Record); + } // ── Data fetching ──────────────────────────────────────────────────── @@ -177,10 +178,12 @@ if (!payload.groupId || payload.groupId === groupId) loadTasks(); } - $effect(() => { + // Use onMount instead of $effect — loadTasks() writes to $state (pollToken++) + // which would trigger $effect re-run → infinite loop + import { onMount } from 'svelte'; + onMount(() => { loadTasks(); appRpc.addMessageListener('bttask.changed', onTaskChanged); - // Feature 4: Fallback 30s poll for missed events pollTimer = setInterval(loadTasks, 30000); return () => { if (pollTimer) clearInterval(pollTimer); @@ -243,11 +246,11 @@ >
{COL_LABELS[col]} - {tasksByCol[col]?.length ?? 0} + {getTasksByCol()[col]?.length ?? 0}
- {#each tasksByCol[col] ?? [] as task (task.id)} + {#each getTasksByCol()[col] ?? [] as task (task.id)}