/** * Server-Sent Events: Format und Grenzen (#033). * * Framework-frei, weil beides geprueft werden muss, ohne eine Verbindung * aufzubauen. Das Format ist winzig und hat genau eine Falle, die einen Strom * still zerstoert — siehe `sseEreignis`. */ /** Der Kanal, auf dem die Datenbank Aenderungen meldet. Muss zur Migration passen. */ export const KANAL = "lauf_geaendert"; /** * Ein Ereignis im SSE-Format. * * **Die Falle:** ein Zeilenumbruch in den Daten beendet das Feld. Steht in der * Nutzlast ein `\n`, zerfaellt die Nachricht, und der Client bekommt Schrott * oder gar nichts. Deshalb wird **jede** Zeile mit eigenem `data:` gesendet — * so schreibt es die Spezifikation vor, und der Client fuegt sie mit `\n` * wieder zusammen. * * Unsere Nutzlast ist JSON aus `JSON.stringify` ohne Einrueckung und hat heute * nie einen Umbruch. „Hat heute nie“ ist aber keine Zusage, sondern ein Zufall: * ein Lauftitel mit Umbruch, ein Notiztext, und der Strom reisst. */ export function sseEreignis(name: string, daten: string): string { const zeilen = daten.split("\n").map((zeile) => `data: ${zeile}`); return `event: ${name}\n${zeilen.join("\n")}\n\n`; } /** * Der Herzschlag: ein Kommentar, den der Client verwirft. * * Ohne ihn schliesst ein Reverse Proxy die stille Verbindung nach seiner * Leerlaufzeit — nginx nach 60 s, und der Browser sieht nur einen Abbruch. * Ein Kommentar (Zeile mit `:`) kostet zwei Bytes und haelt sie offen. */ export const HERZSCHLAG = ": hz\n\n"; const STANDARD_HERZSCHLAG_MS = 25_000; const STANDARD_MAX_STROEME = 200; /** * Abstand der Herzschlaege. * * 25 s, also unter der Leerlaufzeit von 60 s, die nginx und Caddy voreingestellt * haben. Umstellbar, weil der Test nicht eine Minute warten soll, um zu sehen, * dass ueberhaupt einer kommt. */ export function herzschlagMs(): number { const roh = Number.parseInt(process.env["SSE_HERZSCHLAG_MS"]?.trim() ?? "", 10); return Number.isFinite(roh) && roh > 0 ? roh : STANDARD_HERZSCHLAG_MS; } /** * Hoechstzahl gleichzeitig offener Stroeme je Instanz. * * Ein Strom belegt eine HTTP-Verbindung, solange die Lasche offen ist. Ohne * Grenze belegt ein Haus mit vierzig offenen Laschen vierzig Verbindungen, und * irgendwann nimmt der Server keine gewoehnliche Anfrage mehr an — die Seite * waere dann nicht langsam, sondern tot. Ist die Grenze erreicht, antwortet der * Handler gewoehnlich mit JSON, und der Client fuellt auf Polling zurueck. */ export function maxStroeme(): number { const roh = Number.parseInt(process.env["SSE_MAX_STROEME"]?.trim() ?? "", 10); return Number.isFinite(roh) && roh > 0 ? roh : STANDARD_MAX_STROEME; } /** * Will der Aufrufer einen Strom? * * `EventSource` schickt `Accept: text/event-stream` von sich aus — das ist der * einzige Unterschied zwischen dem Strom und dem Polling, und deshalb liegen * beide an **derselben** Adresse: eine Rechtepruefung, ein Lesemodell, eine * Wahrheit. Ein zweiter Endpunkt waere eine zweite Stelle, an der jemand das * Recht vergessen kann. */ export function willStrom(accept: string | null): boolean { return (accept ?? "").split(",").some((teil) => teil.trim().startsWith("text/event-stream")); }