import { useCallback, useEffect, useRef, useState } from 'react' import { clearHistory, fetchHistory, isWebhookEvent, mergeEvent, mergeEvents, type WebhookEvent, } from '../webhook' export type WebhookStatus = 'connecting' | 'open' | 'closed' export interface WebhookState { events: WebhookEvent[] status: WebhookStatus error: string | null clear: () => Promise } /** * Hört auf die Nachrichten des Webhooks. * * Der Strom läuft über eine `EventSource` auf `/api/webhook/events` und wird * **unabhängig vom geöffneten Bereich** gehalten: So ist schon alles da, wenn * der Bereich „Webhook" aufgeschlagen wird. Bricht die Verbindung ab, verbindet * der Browser von selbst neu und schickt dabei die `Last-Event-ID` mit – das * Backend liefert dann nach, was in der Lücke ankam. * * Zusätzlich wird beim Start der Verlauf geholt, damit auch Nachrichten von vor * dem Laden der Seite erscheinen. */ export function useWebhook(): WebhookState { const [events, setEvents] = useState([]) const [status, setStatus] = useState('connecting') const [error, setError] = useState(null) const sourceRef = useRef(null) useEffect(() => { const controller = new AbortController() fetchHistory(controller.signal) .then((history) => { if (controller.signal.aborted) return setEvents((current) => mergeEvents(current, history)) }) .catch((e: unknown) => { if (controller.signal.aborted) return setError(e instanceof Error ? e.message : String(e)) }) const source = new EventSource('/api/webhook/events') sourceRef.current = source source.onopen = () => { setStatus('open') setError(null) } source.addEventListener('webhook', (event) => { try { const parsed: unknown = JSON.parse((event as MessageEvent).data) if (isWebhookEvent(parsed)) setEvents((current) => mergeEvent(current, parsed)) } catch { // Unlesbare Nachricht: lieber übergehen als die Anzeige abstürzen lassen. } }) source.onerror = () => { // Der Browser verbindet von selbst neu; nur der endgültige Abbruch ist final. setStatus(source.readyState === EventSource.CLOSED ? 'closed' : 'connecting') } return () => { controller.abort() source.close() sourceRef.current = null } }, []) const clear = useCallback(async () => { try { await clearHistory() setEvents([]) setError(null) } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)) } }, []) return { events, status, error, clear } }