fix(electrobun): eliminate $effect/$derived cycles in 3 more components
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.
This commit is contained in:
parent
8d09632879
commit
20e4d2cdec
3 changed files with 19 additions and 16 deletions
|
|
@ -186,11 +186,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
// Use onMount instead of $effect — loadChannels/loadAgents write to $state
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
onMount(() => {
|
||||||
loadChannels();
|
loadChannels();
|
||||||
loadAgents();
|
loadAgents();
|
||||||
appRpc.addMessageListener('btmsg.newMessage', onNewMessage);
|
appRpc.addMessageListener('btmsg.newMessage', onNewMessage);
|
||||||
// Feature 4: Fallback 30s poll for missed events
|
|
||||||
pollTimer = setInterval(() => {
|
pollTimer = setInterval(() => {
|
||||||
if (mode === 'channels' && activeChannelId) {
|
if (mode === 'channels' && activeChannelId) {
|
||||||
loadChannelMessages(activeChannelId);
|
loadChannelMessages(activeChannelId);
|
||||||
|
|
@ -198,7 +199,6 @@
|
||||||
loadDmMessages(activeDmAgentId);
|
loadDmMessages(activeDmAgentId);
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (pollTimer) clearInterval(pollTimer);
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
appRpc.removeMessageListener?.('btmsg.newMessage', onNewMessage);
|
appRpc.removeMessageListener?.('btmsg.newMessage', onNewMessage);
|
||||||
|
|
|
||||||
|
|
@ -263,13 +263,13 @@
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load root directory on mount
|
// Load root directory on mount — use onMount, NOT $effect
|
||||||
$effect(() => {
|
// $effect reads openDirs (via new Set(openDirs)) and writes to it → infinite loop
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
onMount(() => {
|
||||||
if (cwd) {
|
if (cwd) {
|
||||||
loadDir(cwd);
|
loadDir(cwd);
|
||||||
const s = new Set(openDirs);
|
openDirs = new Set([cwd]);
|
||||||
s.add(cwd);
|
|
||||||
openDirs = s;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,13 @@
|
||||||
|
|
||||||
// ── Derived ──────────────────────────────────────────────────────────
|
// ── Derived ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let tasksByCol = $derived(
|
// NO $derived — .reduce creates new objects every evaluation → infinite loop
|
||||||
COLUMNS.reduce((acc, col) => {
|
function getTasksByCol(): Record<string, Task[]> {
|
||||||
|
return COLUMNS.reduce((acc, col) => {
|
||||||
acc[col] = tasks.filter(t => t.status === col);
|
acc[col] = tasks.filter(t => t.status === col);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, Task[]>)
|
}, {} as Record<string, Task[]>);
|
||||||
);
|
}
|
||||||
|
|
||||||
// ── Data fetching ────────────────────────────────────────────────────
|
// ── Data fetching ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -177,10 +178,12 @@
|
||||||
if (!payload.groupId || payload.groupId === groupId) loadTasks();
|
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();
|
loadTasks();
|
||||||
appRpc.addMessageListener('bttask.changed', onTaskChanged);
|
appRpc.addMessageListener('bttask.changed', onTaskChanged);
|
||||||
// Feature 4: Fallback 30s poll for missed events
|
|
||||||
pollTimer = setInterval(loadTasks, 30000);
|
pollTimer = setInterval(loadTasks, 30000);
|
||||||
return () => {
|
return () => {
|
||||||
if (pollTimer) clearInterval(pollTimer);
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
|
@ -243,11 +246,11 @@
|
||||||
>
|
>
|
||||||
<div class="tb-col-header">
|
<div class="tb-col-header">
|
||||||
<span class="tb-col-label">{COL_LABELS[col]}</span>
|
<span class="tb-col-label">{COL_LABELS[col]}</span>
|
||||||
<span class="tb-col-count">{tasksByCol[col]?.length ?? 0}</span>
|
<span class="tb-col-count">{getTasksByCol()[col]?.length ?? 0}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tb-col-body">
|
<div class="tb-col-body">
|
||||||
{#each tasksByCol[col] ?? [] as task (task.id)}
|
{#each getTasksByCol()[col] ?? [] as task (task.id)}
|
||||||
<div
|
<div
|
||||||
class="tb-card"
|
class="tb-card"
|
||||||
class:dragging={draggedTaskId === task.id}
|
class:dragging={draggedTaskId === task.id}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue