title: The live tier order: 2

The live tier

A WebSocket route is declared like any other route, because it is one:

websocket '/ws/:room' => 'WS::Chat#join_room', {
    protocols        => [ 'punk.chat.v1' ],
    max_message_size => 65_536,
};

Why it routes like a GET

An upgrade request is a GET, so the route sits in the same router as everything else and under the same scopes and guards. A guard can therefore reject a client with an ordinary HTTP response before the handshake happens, which is the only point at which refusing is cheap.

The handler

Once Punk has validated and answered the handshake, the handler is called with the context and the connection:

sub join_room {
    my ($c, $ws) = @_;
    my $room = Chat::Bus->room($c->param('room'));
    $room->add($ws);
    $ws->on(message => sub {
        my ($conn, $text) = @_;
        $room->broadcast($text);
    });
}

It wires the events it wants and returns. The connection then lives on the server's event loop rather than in the handler's stack frame, so a worker is not pinned per connection.

Broadcasting

Chat::Bus keeps one Punk::WebSocket::Room per room name. Anything that can reach the bus can broadcast, which is how a message arriving through the API reaches open browser tabs. See the API tier.

The browser side

root/static/chat.js is a plain WebSocket client with no framework. It offers the punk.chat.v1 subprotocol, which the route requires, so a client that offers none of the protocols listed is refused at the handshake.