0 ) { $buffer = ob_get_status(); if ( empty( $buffer['flags'] ) || ! ( (int) $buffer['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE ) || ! ob_end_clean() ) { break; } } if ( ! headers_sent() ) { status_header( 200 ); header( 'Content-Type: text/event-stream; charset=utf-8' ); header( 'Cache-Control: no-cache, no-store, no-transform' ); // nginx: do not buffer this response. header( 'X-Accel-Buffering: no' ); header( 'Connection: keep-alive' ); } // Padding comment defeats small intermediary buffers. echo ':' . esc_html( str_repeat( ' ', 2048 ) ) . "\n\n"; $this->flush(); } /** * Sends one event. * * @param string $event Event name. * @param array $data Payload (JSON encoded). */ public function send( string $event, array $data ): void { if ( $this->gone ) { return; } $event = preg_replace( '/[^a-z_]/', '', $event ); // JSON on a single line; the browser parses it and never injects it as HTML. echo 'event: ' . esc_html( $event ) . "\n" . 'data: ' . wp_json_encode( $data, JSON_UNESCAPED_UNICODE ) . "\n\n"; $this->flush(); } /** * Sends an SSE comment when nothing was written for HEARTBEAT_SECONDS, so proxies and * browsers do not treat a long model "think" as an idle connection. */ public function heartbeat(): void { if ( ! $this->gone && microtime( true ) - $this->last_write >= self::HEARTBEAT_SECONDS ) { echo ": ping\n\n"; $this->flush(); } } /** * Whether the browser went away (only known after a write). */ public function client_gone(): bool { if ( ! $this->gone && connection_aborted() ) { $this->gone = true; } return $this->gone; } /** * Flushes output to the client. */ private function flush(): void { if ( ob_get_level() > 0 ) { ob_flush(); } flush(); $this->last_write = microtime( true ); } /** * Ends the request (bypasses the REST server's JSON output). */ public function finish(): void { exit; } }