"use client"; import * as React from "react"; /** * Die laufende Uhr der Überwachung. * * Ein Hook, der einmal je Sekunde einen neuen Zeitpunkt liefert — mehr nicht. * **Er zählt nichts herunter und merkt sich nichts.** Alle Dauern entstehen in * den Ansichten aus zwei Zeitstempeln: dem vom Server und diesem hier. * * Genau daran hängt die wichtigste Anforderung dieser Phase: ein Neuladen des * Browsers und ein Verbindungsabbruch dürfen die Anzeige nicht verfälschen. * Ein heruntergezählter Wert wäre nach einem Neuladen verloren; ein * Zeitstempel aus der Datenbank ist es nicht. */ export function useJetzt(taktMs = 1000): Date { const [jetzt, setJetzt] = React.useState(() => new Date()); React.useEffect(() => { const zeitgeber = setInterval(() => setJetzt(new Date()), taktMs); return () => clearInterval(zeitgeber); }, [taktMs]); return jetzt; } /** * Der Ereignisstrom der Überwachung. * * Verbindet sich mit /api/atemschutz/strom und ruft `beiAenderung` auf, sobald * sich am Serverstand etwas getan hat. Bricht die Verbindung ab, verbindet * `EventSource` von selbst neu — deshalb steht hier keine Zeile * Wiederverbindungslogik. * * Zusätzlich meldet der Hook, ob die Verbindung gerade steht. Das ist kein * Beiwerk: wer am Bildschirm nicht sieht, dass die Verbindung weg ist, hält * einen alten Stand für den aktuellen. Steht sie nicht, fragt die Ansicht in * einem festen Takt nach — der Strom macht sie schneller, nicht erst * funktionsfähig. */ /** * Nach dieser Stille gilt die Verbindung als tot. * * Der Server schickt alle zwei Sekunden ein Lebenszeichen. Bleibt es aus, ist * der Draht weg — auch wenn der Browser das nicht meldet. Genau das ist der * Fall, den ein E2E-Test aufgedeckt hat: wird das Netz gekappt, während der * Strom offen ist, schweigt `EventSource` und meldet **keinen** Fehler. Ohne * diese Frist sähe eine tote Verbindung aus wie eine gesunde, und das ist der * gefährlichste Zustand, den diese Ansicht haben kann. */ const STILLE_MS = 8000; export function useEreignisstrom( kennung: string | null, beiAenderung: () => void, /** * Adresse des Stroms, an die die Kennung angehängt wird. * * Vorgabe ist die Atemschutzüberwachung; die Lagekarte aus Phase 6 benutzt * denselben Hook mit ihrer eigenen Adresse. Zwei Ereignisströme mit * unterschiedlichem Verhalten wären ein Ort mehr, an dem sich ein Fehler * verstecken kann. */ adresse = "/api/atemschutz/strom?id=", ): { verbunden: boolean; letzteMeldung: Date | null } { const [verbunden, setVerbunden] = React.useState(false); const [letzteMeldung, setLetzteMeldung] = React.useState(null); // Als Referenz, damit die Wache unten nicht bei jeder Meldung neu aufgezogen // wird — dieselbe Falle wie in der Warteschlange des Terminals. const letzteRef = React.useRef(0); // Über eine Referenz, damit ein neuer Rückruf nicht die Verbindung neu // aufbaut — sonst risse der Strom bei jedem Rendern ab. const rueckruf = React.useRef(beiAenderung); rueckruf.current = beiAenderung; React.useEffect(() => { if (!kennung || typeof window === "undefined") return; const quelle = new EventSource(`${adresse}${encodeURIComponent(kennung)}`); const gehoert = () => { letzteRef.current = Date.now(); setVerbunden(true); setLetzteMeldung(new Date()); }; quelle.onopen = gehoert; quelle.addEventListener("aenderung", () => { gehoert(); rueckruf.current(); }); quelle.addEventListener("takt", gehoert); quelle.onerror = () => { // EventSource versucht es von selbst wieder. Hier wird nur vermerkt, // dass der Stand gerade nicht nachgeführt wird. setVerbunden(false); }; // Die Wache: bleibt das Lebenszeichen aus, gilt die Verbindung als tot — // unabhaengig davon, was EventSource meldet. const wache = setInterval(() => { if (letzteRef.current > 0 && Date.now() - letzteRef.current > STILLE_MS) { setVerbunden(false); } }, 1000); return () => { clearInterval(wache); quelle.close(); }; }, [kennung, adresse]); return { verbunden, letzteMeldung }; }