NAME
PAGI::Endpoint::WebSocket - Class-based WebSocket endpoint handler
SYNOPSIS
package MyApp::Chat;
use parent 'PAGI::Endpoint::WebSocket';
use Future::AsyncAwait;
sub encoding { 'json' } # or 'text', 'bytes'
sub ping_interval { 25 } # Send ping every 25 seconds
async sub on_connect {
my ($self, $ws) = @_;
await $ws->accept;
await $ws->send_json({ type => 'welcome' });
}
async sub on_receive {
my ($self, $ws, $data) = @_;
# $data is auto-decoded from JSON (per encoding above)
# For sending, explicitly choose: send_json, send_text, send_bytes
await $ws->send_json({ type => 'echo', message => $data });
}
sub on_disconnect {
my ($self, $ws, $code) = @_;
cleanup_user($ws->stash->{user_id});
}
# Use with PAGI server
my $app = MyApp::Chat->to_app;
DESCRIPTION
PAGI::Endpoint::WebSocket provides a Starlette-inspired class-based approach to handling WebSocket connections with lifecycle hooks.
Connection Keepalive
WebSocket connections may be closed by proxies, load balancers, or NAT devices after periods of inactivity (typically 30-60 seconds). To prevent this, enable server-side ping by overriding ping_interval:
sub ping_interval { 25 } # Send ping every 25 seconds
The server sends JSON messages { type => 'ping', ts => <timestamp> } at the specified interval. Clients can optionally respond with { type => 'pong' } to confirm receipt, though this is not required for keepalive purposes.
LIFECYCLE METHODS
on_connect
async sub on_connect {
my ($self, $ws) = @_;
await $ws->accept;
}
Called when a client connects. You should call $ws->accept to accept the connection. If not defined, connection is auto-accepted.
on_receive
async sub on_receive {
my ($self, $ws, $data) = @_;
await $ws->send_text("Got: $data");
}
Called for each message received. The $data format depends on the encoding() setting.
on_disconnect
sub on_disconnect {
my ($self, $ws, $code, $reason) = @_;
# Cleanup
}
Called when connection closes. This is synchronous (not async).
CLASS METHODS
encoding
sub encoding { 'json' } # 'text', 'bytes', or 'json'
Controls how incoming messages are decoded before being passed to on_receive. This does not affect outgoing messages - you always explicitly choose the send method (send_json, send_text, send_bytes).
text- Messages passed as strings (default)bytes- Messages passed as raw bytesjson- Messages automatically decoded from JSON to Perl data structures
Example - JSON encoding:
package MyEndpoint;
use parent 'PAGI::Endpoint::WebSocket';
sub encoding { 'json' } # Incoming messages auto-decoded from JSON
async sub on_receive {
my ($self, $ws, $data) = @_;
# $data is already a Perl hashref/arrayref (decoded from JSON)
my $name = $data->{name};
# For sending, you still explicitly choose the method:
await $ws->send_json({ greeting => "Hello, $name" });
await $ws->send_text("Raw text message");
}
Example - Text encoding:
sub encoding { 'text' } # Incoming messages as raw strings
async sub on_receive {
my ($self, $ws, $text) = @_;
# $text is a plain string, decode JSON yourself if needed
my $data = JSON::MaybeXS::decode_json($text);
await $ws->send_text("Echo: $text");
}
This follows the same pattern as Starlette's WebSocketEndpoint.
ping_interval
sub ping_interval { 25 } # seconds, 0 = disabled (default)
Seconds between server-initiated ping messages. Set to a positive value to enable keepalive pings that prevent proxy/NAT timeouts on idle connections.
When enabled, the server sends JSON messages at the specified interval:
{ "type": "ping", "ts": 1703275200 }
Common values:
25- Safe for most proxies (30s timeout common)55- Safe for aggressive proxies (60s timeout)0- Disabled (default) - connection may timeout if idle
Note: Requires the PAGI event loop to be available in the scope (automatically provided by PAGI::Server).
websocket_class
sub websocket_class { 'PAGI::WebSocket' }
Override to use a custom WebSocket wrapper.
to_app
my $app = MyEndpoint->to_app;
Returns a PAGI-compatible async coderef.