From a90d7d06c4cb2ef4d951def2614fcea4001308bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Sat, 11 Apr 2026 21:44:19 +1000 Subject: [PATCH] fix: stop os search modal and sse reconnect loops --- app/api/events/route.ts | 32 +++++++++++----------- src/components/layout/ThreePanelLayout.tsx | 20 +++++++++++--- src/components/nodes/SearchModal.tsx | 5 ++-- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/app/api/events/route.ts b/app/api/events/route.ts index 792a87d..2432559 100644 --- a/app/api/events/route.ts +++ b/app/api/events/route.ts @@ -5,11 +5,21 @@ import { eventBroadcaster } from '@/services/events'; -export async function GET() { +export async function GET(request: Request) { const encoder = new TextEncoder(); + let activeController: ReadableStreamDefaultController | null = null; + + const cleanupConnection = () => { + if (!activeController) return; + console.log('🔌 SSE connection cleanup'); + eventBroadcaster.removeConnection(activeController); + activeController = null; + }; const stream = new ReadableStream({ start(controller) { + activeController = controller; + // Add this connection to the broadcaster console.log('🔌 New SSE connection established'); eventBroadcaster.addConnection(controller); @@ -22,25 +32,15 @@ export async function GET() { })}\n\n`; controller.enqueue(encoder.encode(initialMessage)); - - // Store controller reference for cleanup - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (controller as any)._cleanup = () => { - console.log('🔌 SSE connection cleanup'); - eventBroadcaster.removeConnection(controller); - }; }, - cancel(controller) { - // Clean up when client disconnects - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((controller as any)._cleanup) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (controller as any)._cleanup(); - } + cancel() { + cleanupConnection(); } }); + request.signal.addEventListener('abort', cleanupConnection, { once: true }); + return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', @@ -50,4 +50,4 @@ export async function GET() { 'Access-Control-Allow-Headers': 'Cache-Control', }, }); -} \ No newline at end of file +} diff --git a/src/components/layout/ThreePanelLayout.tsx b/src/components/layout/ThreePanelLayout.tsx index 22f0a07..37ec274 100644 --- a/src/components/layout/ThreePanelLayout.tsx +++ b/src/components/layout/ThreePanelLayout.tsx @@ -45,6 +45,8 @@ const DEFAULT_SLOT_A: SlotState = { activeTabId: 'views', }; +const EMPTY_SEARCH_FILTERS: Array<{ type: 'context' | 'title' | 'tag'; value: string }> = []; + function createSingletonState(type: Exclude): SlotState { return { tabs: [{ id: createTabId(type), type }], @@ -157,6 +159,8 @@ export default function ThreePanelLayout() { } | null>(null); const [pendingNodes, setPendingNodes] = useState([]); const [dragOverSlot, setDragOverSlot] = useState(null); + const openNodeIdsRef = useRef([]); + const handleNodeDeletedRef = useRef<(nodeId: number) => void>(() => {}); const handleCloseSettings = useCallback(() => { setShowSettings(false); @@ -268,6 +272,10 @@ export default function ThreePanelLayout() { return [...ids]; }, [slotStates]); + useEffect(() => { + openNodeIdsRef.current = allOpenNodeIds; + }, [allOpenNodeIds]); + const activeNodeId = useMemo(() => { const activeSlotState = slotStates[activePane]; const activeTab = activeSlotState ? getActiveTab(activeSlotState) : undefined; @@ -357,12 +365,12 @@ export default function ThreePanelLayout() { break; case 'NODE_UPDATED': setNodesPanelRefresh((prev) => prev + 1); - if (allOpenNodeIds.includes(Number(data.data.nodeId))) { + if (openNodeIdsRef.current.includes(Number(data.data.nodeId))) { setFocusPanelRefresh((prev) => prev + 1); } break; case 'NODE_DELETED': - handleNodeDeleted(Number(data.data.nodeId)); + handleNodeDeletedRef.current(Number(data.data.nodeId)); setNodesPanelRefresh((prev) => prev + 1); break; case 'EDGE_CREATED': @@ -401,7 +409,7 @@ export default function ThreePanelLayout() { return () => { eventSource?.close(); }; - }, [allOpenNodeIds]); + }, []); useEffect(() => { if (pendingNodes.length === 0) return; @@ -535,6 +543,10 @@ export default function ThreePanelLayout() { (['A', 'B', 'C'] as SlotId[]).forEach((slot) => closeTabInSlot(slot, tabId)); }, [closeTabInSlot]); + useEffect(() => { + handleNodeDeletedRef.current = handleNodeDeleted; + }, [handleNodeDeleted]); + const handleReorderTabs = useCallback((slot: SlotId, fromIndex: number, toIndex: number) => { const state = getSlotState(slot); if (!state || fromIndex === toIndex) return; @@ -981,7 +993,7 @@ export default function ThreePanelLayout() { openNodeFromSlot(nodeId); setShowSearchModal(false); }} - existingFilters={[]} + existingFilters={EMPTY_SEARCH_FILTERS} /> { if (!searchQuery.trim()) { - setSuggestions([]); + setSuggestions((prev) => (prev.length === 0 ? prev : [])); + setSelectedIndex(0); return; } @@ -122,7 +123,7 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil const timeoutId = setTimeout(fetchSuggestions, 200); return () => clearTimeout(timeoutId); - }, [searchQuery, existingFilters]); + }, [searchQuery]); // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => {