phasync
WebSockets and SSE / ServerSentEvents

Class

ServerSentEvents

A Server-Sent Events stream (EventSource in the browser) on a ClientRequest.

Introduction#

Creating it sends the head (200, content-type: text/event-stream, cache-control: no-cache) and puts it on the wire at once, so the browser sees the stream is open before the first event exists. The body is chunked on HTTP/1.1 and ends with the connection on HTTP/1.0; each event is written as one chunk. The stream ends when the handler returns.

A client that leaves makes the next send() or comment() throw phasync\IOException. Let it leave the handler: swerve ends the exchange without logging, so a loop ends with its client. A handler that may wait long between events sends a comment now and then, to notice a departure and to keep proxies from closing the idle stream.

Class synopsis#

final class ServerSentEvents {
    /* Methods */
    public ServerSentEvents::__construct(ClientRequest $request, array $headers = [])
    public ServerSentEvents::lastEventId(): ?string
    public ServerSentEvents::send(
        string $data,
        ?string $event = null,
        ?string $id = null,
        ?int $retry = null
    ): void
    public ServerSentEvents::comment(string $text = ''): void
}

Methods#

MethodDescription
__constructSend the head of the stream.
lastEventIdThe Last-Event-ID the client sent when it reconnected: the id of the last event it saw before the connection dropped, or null at its first connection.
sendSend one event.
commentSend a comment: the browser ignores it, so it keeps the stream alive through proxies and lets a handler find out that the client is gone.

Examples#

Example 1An event stream
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\ServerSentEvents;

$app = new RequestHandler(function (ClientRequest $request) {
    $events = new ServerSentEvents($request);   // the head goes out at once
    foreach (['build started', 'tests passed', 'deployed'] as $n => $status) {
        phasync::sleep(0.05);
        $events->send($status, id: (string) ($n + 1));
    }
});                                            // the stream ends when the handler returns

if (debug_backtrace()) {
    return $app;   // swerve loads this file: it is the application's swerve.php
}

// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /deploys HTTP/1.1\r\nHost: example\r\nAccept: text/event-stream\r\n\r\n"]);
Output
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
x-accel-buffering: no
Server: Swerve
Transfer-Encoding: chunked
Connection: close

1b
id: 1
data: build started


1a
id: 2
data: tests passed


16
id: 3
data: deployed


0
Example 2
return new Swerve\RequestHandler(function (Swerve\ClientRequest $request) {
    $sse = new Swerve\ServerSentEvents($request);
    foreach (Swerve::subscribe('news', $sse->lastEventId()) as $message) {
        $sse->send($message);
    }
});