fix: stop os search modal and sse reconnect loops

This commit is contained in:
“BeeRad”
2026-04-11 21:44:19 +10:00
parent 3ae46245ec
commit a90d7d06c4
3 changed files with 35 additions and 22 deletions
+15 -15
View File
@@ -5,11 +5,21 @@
import { eventBroadcaster } from '@/services/events'; import { eventBroadcaster } from '@/services/events';
export async function GET() { export async function GET(request: Request) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
let activeController: ReadableStreamDefaultController<Uint8Array> | null = null;
const cleanupConnection = () => {
if (!activeController) return;
console.log('🔌 SSE connection cleanup');
eventBroadcaster.removeConnection(activeController);
activeController = null;
};
const stream = new ReadableStream({ const stream = new ReadableStream({
start(controller) { start(controller) {
activeController = controller;
// Add this connection to the broadcaster // Add this connection to the broadcaster
console.log('🔌 New SSE connection established'); console.log('🔌 New SSE connection established');
eventBroadcaster.addConnection(controller); eventBroadcaster.addConnection(controller);
@@ -22,25 +32,15 @@ export async function GET() {
})}\n\n`; })}\n\n`;
controller.enqueue(encoder.encode(initialMessage)); 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) { cancel() {
// Clean up when client disconnects cleanupConnection();
// 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();
}
} }
}); });
request.signal.addEventListener('abort', cleanupConnection, { once: true });
return new Response(stream, { return new Response(stream, {
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
+16 -4
View File
@@ -45,6 +45,8 @@ const DEFAULT_SLOT_A: SlotState = {
activeTabId: 'views', activeTabId: 'views',
}; };
const EMPTY_SEARCH_FILTERS: Array<{ type: 'context' | 'title' | 'tag'; value: string }> = [];
function createSingletonState(type: Exclude<PaneType, 'node'>): SlotState { function createSingletonState(type: Exclude<PaneType, 'node'>): SlotState {
return { return {
tabs: [{ id: createTabId(type), type }], tabs: [{ id: createTabId(type), type }],
@@ -157,6 +159,8 @@ export default function ThreePanelLayout() {
} | null>(null); } | null>(null);
const [pendingNodes, setPendingNodes] = useState<PendingNode[]>([]); const [pendingNodes, setPendingNodes] = useState<PendingNode[]>([]);
const [dragOverSlot, setDragOverSlot] = useState<SlotId | null>(null); const [dragOverSlot, setDragOverSlot] = useState<SlotId | null>(null);
const openNodeIdsRef = useRef<number[]>([]);
const handleNodeDeletedRef = useRef<(nodeId: number) => void>(() => {});
const handleCloseSettings = useCallback(() => { const handleCloseSettings = useCallback(() => {
setShowSettings(false); setShowSettings(false);
@@ -268,6 +272,10 @@ export default function ThreePanelLayout() {
return [...ids]; return [...ids];
}, [slotStates]); }, [slotStates]);
useEffect(() => {
openNodeIdsRef.current = allOpenNodeIds;
}, [allOpenNodeIds]);
const activeNodeId = useMemo(() => { const activeNodeId = useMemo(() => {
const activeSlotState = slotStates[activePane]; const activeSlotState = slotStates[activePane];
const activeTab = activeSlotState ? getActiveTab(activeSlotState) : undefined; const activeTab = activeSlotState ? getActiveTab(activeSlotState) : undefined;
@@ -357,12 +365,12 @@ export default function ThreePanelLayout() {
break; break;
case 'NODE_UPDATED': case 'NODE_UPDATED':
setNodesPanelRefresh((prev) => prev + 1); setNodesPanelRefresh((prev) => prev + 1);
if (allOpenNodeIds.includes(Number(data.data.nodeId))) { if (openNodeIdsRef.current.includes(Number(data.data.nodeId))) {
setFocusPanelRefresh((prev) => prev + 1); setFocusPanelRefresh((prev) => prev + 1);
} }
break; break;
case 'NODE_DELETED': case 'NODE_DELETED':
handleNodeDeleted(Number(data.data.nodeId)); handleNodeDeletedRef.current(Number(data.data.nodeId));
setNodesPanelRefresh((prev) => prev + 1); setNodesPanelRefresh((prev) => prev + 1);
break; break;
case 'EDGE_CREATED': case 'EDGE_CREATED':
@@ -401,7 +409,7 @@ export default function ThreePanelLayout() {
return () => { return () => {
eventSource?.close(); eventSource?.close();
}; };
}, [allOpenNodeIds]); }, []);
useEffect(() => { useEffect(() => {
if (pendingNodes.length === 0) return; if (pendingNodes.length === 0) return;
@@ -535,6 +543,10 @@ export default function ThreePanelLayout() {
(['A', 'B', 'C'] as SlotId[]).forEach((slot) => closeTabInSlot(slot, tabId)); (['A', 'B', 'C'] as SlotId[]).forEach((slot) => closeTabInSlot(slot, tabId));
}, [closeTabInSlot]); }, [closeTabInSlot]);
useEffect(() => {
handleNodeDeletedRef.current = handleNodeDeleted;
}, [handleNodeDeleted]);
const handleReorderTabs = useCallback((slot: SlotId, fromIndex: number, toIndex: number) => { const handleReorderTabs = useCallback((slot: SlotId, fromIndex: number, toIndex: number) => {
const state = getSlotState(slot); const state = getSlotState(slot);
if (!state || fromIndex === toIndex) return; if (!state || fromIndex === toIndex) return;
@@ -981,7 +993,7 @@ export default function ThreePanelLayout() {
openNodeFromSlot(nodeId); openNodeFromSlot(nodeId);
setShowSearchModal(false); setShowSearchModal(false);
}} }}
existingFilters={[]} existingFilters={EMPTY_SEARCH_FILTERS}
/> />
<SettingsModal <SettingsModal
+3 -2
View File
@@ -95,7 +95,8 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil
// Generate suggestions based on search query // Generate suggestions based on search query
useEffect(() => { useEffect(() => {
if (!searchQuery.trim()) { if (!searchQuery.trim()) {
setSuggestions([]); setSuggestions((prev) => (prev.length === 0 ? prev : []));
setSelectedIndex(0);
return; return;
} }
@@ -122,7 +123,7 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil
const timeoutId = setTimeout(fetchSuggestions, 200); const timeoutId = setTimeout(fetchSuggestions, 200);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [searchQuery, existingFilters]); }, [searchQuery]);
// Handle keyboard navigation // Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {