// app/components/SSEContext.tsx 'use client'; import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useCurrentUser } from './AuthContext'; import { Recognition } from '@/types/plates'; type ConnectionStatus = 'connected' | 'connecting' | 'disconnected' | 'error'; type ExportProgressMsg = { jobId: string; stage?: string; done?: number; total?: number; progress?: number; // 0..100 (serverseitig typ. bis 99) }; type SSEContextType = { onNewRecognition: (cb: (r: Recognition) => void) => () => void; // ← gibt Unsubscribe zurück onExportProgress: (cb: (m: ExportProgressMsg) => void) => () => void; // ← neu connectionStatus: ConnectionStatus; newCount: number; resetNewCount: () => void; }; const SSEContext = createContext(undefined); export function SSEProvider({ children }: { children: React.ReactNode }) { const { user } = useCurrentUser(); // Listener-Sets statt Arrays → leichtes Unsubscribe & keine Duplikate const recListeners = useRef void>>(new Set()); const exportListeners= useRef void>>(new Set()); const [status, setStatus] = useState('disconnected'); const [newCount, setNewCount] = useState(0); useEffect(() => { if (!user) { setStatus('disconnected'); return; } setStatus('connecting'); const es = new EventSource('/api/recognitions/stream', { withCredentials: true }); es.onopen = () => setStatus('connected'); es.addEventListener('new-recognition', (e) => { try { const rec = JSON.parse((e as MessageEvent).data) as Recognition; recListeners.current.forEach((cb) => { try { cb(rec); } catch {} }); setNewCount((c) => c + 1); } catch {} }); // Optional: ping nur, damit Verbindung „lebt“ es.addEventListener('ping', () => { /* noop */ }); // NEU: Export-Fortschritt es.addEventListener('export-progress', (e) => { try { const msg = JSON.parse((e as MessageEvent).data) as ExportProgressMsg; if (!msg || !msg.jobId) return; exportListeners.current.forEach((cb) => { try { cb(msg); } catch {} }); } catch {} }); // Server-initiierter Logout es.addEventListener('logout', (e) => { try { const { reason } = JSON.parse((e as MessageEvent).data); console.info('Server verlangt Logout:', reason); } catch {} fetch('/api/logout', { method: 'POST', credentials: 'include' }) .finally(() => (window.location.href = '/login')); }); es.onerror = (err) => { console.warn('SSE-Fehler:', err); // EventSource versucht selbst zu reconnecten; wir zeigen "error"/"connecting". setStatus('error'); // kurze Zeit später wieder als "connecting" markieren setTimeout(() => setStatus('connecting'), 1000); }; return () => { es.close(); setStatus('disconnected'); }; }, [user]); // neu verbinden, wenn sich der eingeloggte User ändert // Registrieren & Unsubscribe zurückgeben const onNewRecognition = useCallback((cb: (r: Recognition) => void) => { recListeners.current.add(cb); return () => recListeners.current.delete(cb); }, []); const onExportProgress = useCallback((cb: (m: ExportProgressMsg) => void) => { exportListeners.current.add(cb); return () => exportListeners.current.delete(cb); }, []); const ctx: SSEContextType = { onNewRecognition, onExportProgress, // ← im Frontend verwenden connectionStatus: status, newCount, resetNewCount: () => setNewCount(0), }; return {children}; } export function useSSE() { const ctx = useContext(SSEContext); if (!ctx) throw new Error('useSSE must be used inside SSEProvider'); return ctx; }