import React, { createContext, useContext, useEffect, useRef, useState, ReactNode } from 'react'; import { useAuthStore } from '../stores/auth.store'; import { useUIStore } from '../stores/ui.store'; import { useCurrentTenant } from '../stores/tenant.store'; interface SSEEvent { type: string; data: any; timestamp: string; } interface SSEContextType { isConnected: boolean; lastEvent: SSEEvent | null; connect: () => void; disconnect: () => void; addEventListener: (eventType: string, callback: (data: any) => void) => () => void; } const SSEContext = createContext(undefined); export const useSSE = () => { const context = useContext(SSEContext); if (context === undefined) { throw new Error('useSSE must be used within an SSEProvider'); } return context; }; interface SSEProviderProps { children: ReactNode; } export const SSEProvider: React.FC = ({ children }) => { const [isConnected, setIsConnected] = useState(false); const [lastEvent, setLastEvent] = useState(null); const eventSourceRef = useRef(null); const eventListenersRef = useRef void>>>(new Map()); const reconnectTimeoutRef = useRef(); const reconnectAttempts = useRef(0); const { isAuthenticated, token } = useAuthStore(); const { showToast } = useUIStore(); const currentTenant = useCurrentTenant(); const connect = () => { if (!isAuthenticated || !token || eventSourceRef.current) return; // Skip SSE connection for demo/development mode when no backend is available if (token === 'mock-jwt-token') { console.log('SSE connection skipped for demo mode'); return; } // Get tenant ID from store - no fallback const tenantId = currentTenant?.id; if (!tenantId) { console.log('No tenant ID available, skipping SSE connection'); return; } try { // Connect to notification service SSE endpoint with token const eventSource = new EventSource(`http://localhost:8006/api/v1/sse/alerts/stream/${tenantId}?token=${encodeURIComponent(token)}`, { withCredentials: true, }); eventSource.onopen = () => { console.log('SSE connection opened'); setIsConnected(true); reconnectAttempts.current = 0; if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = undefined; } }; eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); // Handle different SSE message types from notification service if (data.status === 'keepalive') { console.log('SSE keepalive received'); return; } const sseEvent: SSEEvent = { type: data.item_type || 'message', data: data, timestamp: data.timestamp || new Date().toISOString(), }; setLastEvent(sseEvent); // Show notification if it's an alert or recommendation if (data.item_type && ['alert', 'recommendation'].includes(data.item_type)) { let toastType: 'info' | 'success' | 'warning' | 'error' = 'info'; if (data.item_type === 'alert') { if (data.severity === 'urgent') toastType = 'error'; else if (data.severity === 'high') toastType = 'error'; else if (data.severity === 'medium') toastType = 'warning'; else toastType = 'info'; } else if (data.item_type === 'recommendation') { toastType = 'info'; } showToast({ type: toastType, title: data.title || 'Notificación', message: data.message, duration: data.severity === 'urgent' ? 0 : 5000, }); } // Trigger registered listeners const listeners = eventListenersRef.current.get(sseEvent.type); if (listeners) { listeners.forEach(callback => callback(data)); } // Also trigger 'message' listeners for backward compatibility const messageListeners = eventListenersRef.current.get('message'); if (messageListeners) { messageListeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing SSE message:', error); } }; // Handle connection confirmation from notification service eventSource.addEventListener('connected', (event) => { try { const data = JSON.parse(event.data); console.log('SSE connection confirmed:', data); } catch (error) { console.error('Error parsing connected event:', error); } }); // Handle ping events (keepalive) eventSource.addEventListener('ping', (event) => { try { const data = JSON.parse(event.data); console.log('SSE ping received:', data.timestamp); } catch (error) { console.error('Error parsing ping event:', error); } }); // Handle initial items eventSource.addEventListener('initial_items', (event) => { try { const data = JSON.parse(event.data); console.log('Initial items received:', data); // Trigger listeners for initial data const listeners = eventListenersRef.current.get('initial_items'); if (listeners) { listeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing initial_items event:', error); } }); // Handle alert events eventSource.addEventListener('alert', (event) => { try { const data = JSON.parse(event.data); const sseEvent: SSEEvent = { type: 'alert', data, timestamp: data.timestamp || new Date().toISOString(), }; setLastEvent(sseEvent); // Show alert toast let toastType: 'info' | 'success' | 'warning' | 'error' = 'info'; if (data.severity === 'urgent') toastType = 'error'; else if (data.severity === 'high') toastType = 'error'; else if (data.severity === 'medium') toastType = 'warning'; else toastType = 'info'; showToast({ type: toastType, title: data.title || 'Alerta', message: data.message, duration: data.severity === 'urgent' ? 0 : 5000, }); // Trigger listeners const listeners = eventListenersRef.current.get('alert'); if (listeners) { listeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing alert event:', error); } }); // Handle recommendation events eventSource.addEventListener('recommendation', (event) => { try { const data = JSON.parse(event.data); const sseEvent: SSEEvent = { type: 'recommendation', data, timestamp: data.timestamp || new Date().toISOString(), }; setLastEvent(sseEvent); // Show recommendation toast showToast({ type: 'info', title: data.title || 'Recomendación', message: data.message, duration: 5000, }); // Trigger listeners const listeners = eventListenersRef.current.get('recommendation'); if (listeners) { listeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing recommendation event:', error); } }); eventSource.onerror = (error) => { console.error('SSE connection error:', error); setIsConnected(false); if (eventSource.readyState === EventSource.CLOSED) { // Attempt reconnection with exponential backoff const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000); reconnectAttempts.current++; reconnectTimeoutRef.current = setTimeout(() => { if (isAuthenticated && token) { connect(); } }, delay); } }; eventSourceRef.current = eventSource; } catch (error) { console.error('Failed to establish SSE connection:', error); setIsConnected(false); } }; const disconnect = () => { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = undefined; } setIsConnected(false); reconnectAttempts.current = 0; }; const addEventListener = (eventType: string, callback: (data: any) => void) => { if (!eventListenersRef.current.has(eventType)) { eventListenersRef.current.set(eventType, new Set()); } eventListenersRef.current.get(eventType)!.add(callback); // Return cleanup function return () => { const listeners = eventListenersRef.current.get(eventType); if (listeners) { listeners.delete(callback); if (listeners.size === 0) { eventListenersRef.current.delete(eventType); } } }; }; // Connect when authenticated, disconnect when not or when tenant changes useEffect(() => { if (isAuthenticated && token && currentTenant) { connect(); } else { disconnect(); } return () => { disconnect(); }; }, [isAuthenticated, token, currentTenant?.id]); // Cleanup on unmount useEffect(() => { return () => { disconnect(); }; }, []); const contextValue: SSEContextType = { isConnected, lastEvent, connect, disconnect, addEventListener, }; return ( {children} ); };