<?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 $timeLimit = 3600; public int $execLimit = 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(); $expirationTime = time() + $this->timeLimit; $lastHeartbeat = time(); while (!connection_aborted() && time() < $expirationTime) { set_time_limit($this->execLimit); try { $data = call_user_func($callback); if ($data !== NULL) { $this->send($data); $lastHeartbeat = time(); } } catch (StopEventLoopException $th) { break; } // sleep and perform heartbeat to ensure connection is still alive for ($i = 0; $i < $this->interval; $i++) { if (time() >= $lastHeartbeat + $this->heartbeat) { echo ": \n\n"; ob_end_flush(); flush(); $lastHeartbeat = time(); } sleep(1); } } } /** * Send data to client encoded as json */ private function send($data): void { echo "data: " . json_encode($data); echo "\n\n"; // send data to stream ob_end_flush(); flush(); } }