Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Server-Sent Events

Whim\HTTP\SSE encodes server-sent events and supplies streaming HTTP response bodies. It ships with stdlib and needs no package or autoloader.

Events

An Event carries UTF-8 text, an optional name, an optional ID, and an optional reconnect delay:

use Whim\HTTP\SSE\Event;
use Whim\Time\Duration;

$event = new Event(
  '{"temperature": 21}',
  name: 'reading',
  id: '42',
  retry: Duration::fromSeconds(3),
);
assert!(
  $event->encode()
  == "id: 42\nevent: reading\nretry: 3000\ndata: "
  . '{"temperature": 21}'
  . "\n\n",
);

encode() writes a data: field for each payload line and ends the event with a blank line. It converts CR and CRLF line endings to LF. Empty data still writes a field, so the browser delivers the event.

Null omits a name or ID. An empty name selects the default message event; an empty ID clears the browser’s resume position. Names and IDs cannot contain line breaks, and IDs cannot contain NUL. All text must be valid UTF-8. Invalid input throws Whim\Unwind\InvalidArgumentException.

Retry durations must be non-negative. Encoding drops fractions of a millisecond and handles Duration::max() without integer overflow.

Comment::of($text) encodes a comment and replaces line breaks with spaces. Browsers ignore comments; a server can use them as heartbeats.

Sources and response bodies

Implement Whim\HTTP\SSE\Source\Source to supply events:

use Whim\Async\CancellationToken;
use Whim\HTTP\SSE;
use Whim\HTTP\SSE\Event;
use Whim\HTTP\SSE\Source\Source;

final class Greetings implements Source {
  private bool $finished = false;

  public function poll(null|CancellationToken $cancellation = null): vec<Event> {
    $cancellation?->throwIfCancellationRequested();
    $this->finished = true;
    return vec[new Event('hello', name: 'greeting', id: '1')];
  }

  public function isFinished(): bool {
    return $this->finished;
  }
}

$response = SSE\respond(new Greetings());
assert!($response->status == 200);
assert!($response->body?->readAll() == "id: 1\nevent: greeting\ndata: hello\n\n");

respond($source, $headers = FieldMap::default()) returns status 200 with Content-Type: text/event-stream; charset=utf-8, Cache-Control: no-store, and X-Accel-Buffering: no. It keeps other caller headers and removes Content-Length and Transfer-Encoding, letting the server choose framing. The HTTP/1.1 server streams chunks; the HTTP/2 server streams DATA frames.

Body implements Whim\IO\ReadHandle and Whim\IO\CloseHandle. A read polls only after it drains buffered events. An empty batch produces Comment::WAITING instead of ending the stream. The body ends after the source finishes and its buffer drains. An already finished source receives no polls.

For a long-lived source, poll() must wait for events or a heartbeat interval and honor its cancellation token. Returning empty batches at once in a loop would consume CPU. Source\FunctionSource wraps a polling callable with the same signature; it never reports itself finished.

The body checks cancellation before and after each poll. Source errors reach the reader unchanged. Closing the body discards buffered bytes and closes a source that implements CloseHandle. A suspended poll cannot refill a body after another task closes it. Use using when reading a body directly. If a handler keeps a body or source, register Body::close() through Context::onResponseComplete() to release it when the response ends.

Reconnection and completion

SSE\last_event_id($request) reads Last-Event-ID, returning null when the field is absent or empty. The application decides how to resume from that ID; the library stores no event history.

SSE\refuse($event, $headers = FieldMap::default()) sends one event with status 200 and ends the response. This alone does not stop reconnection. The client must call EventSource.close() after an application-defined terminal event:

const events = new EventSource('/events');
events.addEventListener('done', () => events.close());
events.addEventListener('failed', () => events.close());

A successful stream ending normally makes EventSource reconnect. To tell a browser to stop without sending an event, return an HTTP 204 response. These rules follow the SSE specification.

See HTTP Server for handlers and response completion, and HTTP Messages and Cookies for response values.