$message]); } /** * @param \Closure $writer - wird von send() mit gesetzten SSE-Headern aufgerufen und * schreibt selbst per echo/flush() an den Client (typischerweise eine Polling-Schleife). */ public static function stream(\Closure $writer): self { return new self(200, null, $writer); } public function isStream(): bool { return $this->streamWriter !== null; } /** Fuer Tests: den Stream-Callback ohne echte HTTP-Header/Output-Handling ausfuehren. */ public function writer(): ?\Closure { return $this->streamWriter; } public function send(): void { // Frontend (html/, Port 8090 im Docker-Compose-Setup) und API (Port 8091) laufen auf // unterschiedlichen Origins - ohne CORS wuerde der Browser sowohl die fetch()-Aufrufe // aus office-api.js als auch den EventSource-Stream (F13) blockieren. Alle Endpunkte // liefern ohnehin nur oeffentliche GET-Daten bzw. verlangen fuer POST/PATCH einen // gueltigen API-Key, ein offener Origin ist daher unkritisch. header('Access-Control-Allow-Origin: *'); if ($this->streamWriter !== null) { http_response_code($this->status); header('Content-Type: text/event-stream; charset=utf-8'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); // Verhindert, dass ein vorgeschalteter nginx die Antwort puffert und SSE-Events // erst nach Verbindungsende ausliefert. header('X-Accel-Buffering: no'); if (function_exists('ob_implicit_flush')) { ob_implicit_flush(true); } while (ob_get_level() > 0) { ob_end_flush(); } ($this->streamWriter)(); return; } http_response_code($this->status); header('Content-Type: application/json; charset=utf-8'); echo json_encode($this->data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } public function body(): mixed { return $this->data; } }