import React, { createContext, useContext, useEffect, useRef, useState, ReactNode } from 'react'; import { useAuthStore } from '../stores/auth.store'; import { useUIStore } from '../stores/ui.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 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; } try { const eventSource = new EventSource(`/api/events?token=${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 sseEvent: SSEEvent = { type: 'message', data: JSON.parse(event.data), timestamp: new Date().toISOString(), }; setLastEvent(sseEvent); // Trigger registered listeners const listeners = eventListenersRef.current.get('message'); if (listeners) { listeners.forEach(callback => callback(sseEvent.data)); } } catch (error) { console.error('Error parsing SSE message:', error); } }; // Handle connection events from backend eventSource.addEventListener('connection', (event) => { try { const data = JSON.parse(event.data); console.log('SSE connection confirmed:', data.message); } catch (error) { console.error('Error parsing connection event:', error); } }); // Handle heartbeat events eventSource.addEventListener('heartbeat', (event) => { try { const data = JSON.parse(event.data); console.log('SSE heartbeat received:', new Date(data.timestamp * 1000)); } catch (error) { console.error('Error parsing heartbeat 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); } }; // Handle notification events (alerts and recommendations from alert_processor) eventSource.addEventListener('notification', (event) => { try { const data = JSON.parse(event.data); const sseEvent: SSEEvent = { type: 'notification', data, timestamp: new Date().toISOString(), }; setLastEvent(sseEvent); // Determine toast type based on severity and 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'; } // Show toast notification showToast({ type: toastType, title: data.title || 'Notificación', message: data.message, duration: data.severity === 'urgent' ? 0 : 5000, // Keep urgent alerts until dismissed }); // Trigger registered listeners const listeners = eventListenersRef.current.get('notification'); if (listeners) { listeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing notification event:', error); } }); eventSource.addEventListener('inventory_alert', (event) => { try { const data = JSON.parse(event.data); const sseEvent: SSEEvent = { type: 'inventory_alert', data, timestamp: new Date().toISOString(), }; setLastEvent(sseEvent); // Show inventory alert (high/urgent alerts from alert_processor) showToast({ type: data.severity === 'urgent' ? 'error' : 'warning', title: data.title || 'Alerta de Inventario', message: data.message, duration: 0, // Keep until dismissed }); // Trigger registered listeners const listeners = eventListenersRef.current.get('inventory_alert'); if (listeners) { listeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing inventory alert:', error); } }); eventSource.addEventListener('production_update', (event) => { try { const data = JSON.parse(event.data); const sseEvent: SSEEvent = { type: 'production_update', data, timestamp: new Date().toISOString(), }; setLastEvent(sseEvent); // Trigger registered listeners const listeners = eventListenersRef.current.get('production_update'); if (listeners) { listeners.forEach(callback => callback(data)); } } catch (error) { console.error('Error parsing production update:', error); } }); 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 useEffect(() => { if (isAuthenticated && token) { connect(); } else { disconnect(); } return () => { disconnect(); }; }, [isAuthenticated, token]); // Cleanup on unmount useEffect(() => { return () => { disconnect(); }; }, []); const contextValue: SSEContextType = { isConnected, lastEvent, connect, disconnect, addEventListener, }; return ( {children} ); };