WebSockets and SSE

Both are route kinds, not add-ons: they sit in the same routing table, under the same scopes and guards as every other route, so a guard can reject a client with an ordinary HTTP response before any upgrade happens.

WebSockets

websocket '/chat' => 'WS::Chat#join';
websocket '/feed' => $target, { protocols => ['v1'] };

An upgrade request routes like a GET. Once the handshake is validated and answered, the handler is called with the context and the live connection:

package MyApp::Controller::WS::Chat;

sub join {
    my ($c, $ws) = @_;

    $ws->on(open => sub {
        my ($ws) = @_;
        Punk::WebSocket::Room->named('lobby')->join($ws);
    });
    $ws->on(message => sub {
        my ($ws, $text) = @_;
        Punk::WebSocket::Room->named('lobby')->broadcast($text);
    });
    $ws->on(close => sub {
        my ($ws, $code, $reason) = @_;
        Punk::WebSocket::Room->named('lobby')->leave($ws);
    });
}

The handler wires the events it wants and returns; the connection then lives on the server's event loop. Punk::WebSocket::Room gives per-worker pub/sub rooms for broadcasting.

Options: protocols (an arrayref of acceptable subprotocols - a client offering none of them is refused), max_message_size (default 16MB), write_buffer_limit, and blocking.

WebSocket routes need Hyperman 0.11 or later, whose detach hands the socket to the application. On other PSGI servers, blocking => 1 runs the connection inside the handler over psgix.io - works anywhere, but pins one worker per connection. Without either, to_app croaks rather than start with routes it cannot serve.

Server-Sent Events

The lighter sibling: a one-way text/event-stream for a browser's EventSource, with heartbeats and reconnect control.

sse '/events' => 'Live#feed';
sse '/events' => $target, { heartbeat => 30 };
package MyApp::Controller::Live;

sub feed {
    my ($c, $stream) = @_;                 # the socket is ours now

    my $tick;
    $tick = sub {
        return unless $stream->is_open;
        $stream->send({ time => time });   # data: {"time":...}
        $c->timer(1)->on_done($tick);      # push once a second
    };
    $tick->();

    $stream->on(close => sub { warn "client gone\n" });
}

send takes a string or a reference (JSON-encoded for you); events can carry a name and id. Options: heartbeat (seconds, default 15), retry (ms), write_buffer_limit, blocking.

Fully non-blocking on a Hyperman worker; portable to any psgi.streaming server; blocking => 1 streams inside the handler over psgix.io anywhere else.

Choosing between them

Reach for SSE when the server pushes and the client only listens - live dashboards, progress, feeds. Reach for WebSockets when the client talks back on the same connection - chat, games, collaborative editing. The Chat example in Punk's example/ directory runs both from one app.