fix: stop os search modal and sse reconnect loops
This commit is contained in:
+16
-16
@@ -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',
|
||||||
@@ -50,4 +50,4 @@ export async function GET() {
|
|||||||
'Access-Control-Allow-Headers': 'Cache-Control',
|
'Access-Control-Allow-Headers': 'Cache-Control',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user