125 lines
3.9 KiB
TypeScript
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;
|
|
}
|