Selector instability caused fetchAndUpdate to recreate on every Zustand
nodes[] mutation (status flips, position drags, peer-discovery writes,
heartbeats — typically ~5/sec). Each recreation invalidated the
useEffect deps so the 60s polling fan-out fired on every update,
hammering /workspaces/<id>/activity?type=delegation 5×N requests/sec
until the edge rate-limit returned 429. User-reported via browser
console showing infinite uE→ux→uE→ux render loop and 429s repeating
across every visible workspace ID.
Root cause:
const nodes = useCanvasStore((s) => s.nodes);
const visibleIds = useMemo(() => nodes.filter(...).map(...), [nodes]);
// useMemo dep recreates on every store update, even when ID set unchanged
Fix: select a STABLE STRING KEY (sorted CSV of visible IDs) from
Zustand. The selector's shallow-equal short-circuit prevents re-renders
when the actual visible-ID set is unchanged, so visibleIds reference
stays stable, fetchAndUpdate keeps its identity, and the useEffect
only re-fires when the visible-ID-set genuinely changes.
Tests:
- New regression test "does not re-fetch when nodes[] reference
changes but visible IDs are the same"
- Discipline-verified: pre-fix code emits 4 fetches (2 mount + 2
re-fetch storm), post-fix emits exactly 2
- Companion test "re-fetches when the visible ID set actually changes"
pins the desired behavior so future "stabilization" doesn't suppress
legitimate updates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>