<?php namespace App\SSE; /** * Small class to abstract Server-Sent Events (SSE) * * https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events */ class EventLoop { public int $interval = 3; public int $heartbeat = 5; // send heartbeat every num seconds to ensure connection is still alive public int $time_limit = 3600; public int $exec_limit = 30; public function start(callable $callback): void { // as session data is locked to prevent concurrent writes we // make it read only to prevent the server from locking up if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } header('Content-Type: text/event-stream'); // explicitly disable caching so varnish and other upstreams won't cache. header('Cache-Control: no-cache, must-revalidate'); // instruct nginx to disable fastcgi_buffering and disable gzip header('X-Accel-Buffering: no'); // send headers to client indicating we are now a stream ob_end_flush(); flush(); $expiration_time = time() + $this->time_limit; $last_heartbeat = time(); while (!connection_aborted() && time() < $expiration_time) { set_time_limit($this->exec_limit); try { $data = call_user_func($callback); if ($data !== NULL) { $this->send($data); $last_heartbeat = time(); } } catch (StopEventLoopException $th) { break; } // sleep and perform heartbeat to ensure connection is still alive for ($i = 0; $i < $this->interval; $i++) { if (time() >= $last_heartbeat + $this->heartbeat) { echo ": \n\n"; ob_end_flush(); flush(); $last_heartbeat = time(); } sleep(1); } } } /** * Send data to client encoded as JSON */ private function send(mixed $data): void { echo "data: " . json_encode($data); echo "\n\n"; // send data to stream ob_end_flush(); flush(); } }