2025-11-10 07:12:06 +01:00

125 lines
3.9 KiB
TypeScript

// 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<SSEContextType | undefined>(undefined);
export function SSEProvider({ children }: { children: React.ReactNode }) {
const { user } = useCurrentUser();
// Listener-Sets statt Arrays → leichtes Unsubscribe & keine Duplikate
const recListeners = useRef<Set<(r: Recognition) => void>>(new Set());
const exportListeners= useRef<Set<(m: ExportProgressMsg) => void>>(new Set());
const [status, setStatus] = useState<ConnectionStatus>('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 <SSEContext.Provider value={ctx}>{children}</SSEContext.Provider>;
}
export function useSSE() {
const ctx = useContext(SSEContext);
if (!ctx) throw new Error('useSSE must be used inside SSEProvider');
return ctx;
}