NAME

PAGI::Context - Per-request context with protocol-specific subclasses

SYNOPSIS

use PAGI::Context;
use Future::AsyncAwait;

# Factory returns the right subclass based on scope type
my $ctx = PAGI::Context->new($scope, $receive, $send);

# Shared methods (all protocol types)
my $type  = $ctx->type;        # 'http', 'websocket', 'sse'
my $path  = $ctx->path;
my $stash = $ctx->stash;       # PAGI::Stash
my $session = $ctx->session;   # PAGI::Session

# WebSocket context - protocol ops directly on $ctx
await $ctx->accept;
await $ctx->send_json({ msg => 'hello' });
my $text = await $ctx->receive_text;
await $ctx->close;

# SSE context - same idea
await $ctx->send_event(event => 'update', data => $payload);
await $ctx->keepalive(25);

# Event dispatcher - works on any protocol type
my $reason = await $ctx
    ->on('websocket.receive', async sub { ... })
    ->on('chat.message',      async sub { ... })
    ->on_error(sub { ... })
    ->run;    # returns 'disconnect', 'stop', or 'error'

# Underlying protocol objects still available
my $ws  = $ctx->websocket;     # PAGI::WebSocket (WS only)
my $sse = $ctx->sse;           # PAGI::SSE (SSE only)
my $req = $ctx->request;       # PAGI::Request (HTTP only)
my $res = $ctx->response;      # PAGI::Response (HTTP only)

DESCRIPTION

PAGI::Context is a factory and base class that provides a unified entry point for per-request context. Calling PAGI::Context->new(...) inspects $scope->{type} and returns the appropriate subclass: PAGI::Context::HTTP, PAGI::Context::WebSocket, or PAGI::Context::SSE.

Shared methods (scope accessors, stash, session, event dispatcher) live on the base class. Protocol-specific methods are delegated from subclasses so you can use $ctx as your single object:

# Instead of:
my $ws = $ctx->websocket;
await $ws->send_json($data);    # closes over $ws in every handler

# Just do:
await $ctx->send_json($data);   # $ctx is already in scope

Protocol Shape

Each context type has a different set of available methods. Calling a method that belongs to a different protocol type raises a standard Perl Can't locate object method error.

Method              HTTP    WebSocket   SSE
──────────────────  ──────  ──────────  ──────
request, response   yes     -           -
method              yes     -           -
accept              -       yes         -
send_text           -       yes         -
send_bytes          -       yes         -
send_json           -       yes         yes
send                -       -           yes
send_event          -       -           yes
send_comment        -       -           yes
start               -       -           yes
close               -       yes         yes
query / query_param -       yes(query)  yes(query_param)
is_connected        base*   WS override -
is_closed           -       yes         yes
is_started          -       -           yes
keepalive           -       yes         yes
each_text, etc.     -       yes         -
each, every         -       -           yes

*is_connected on WebSocket contexts checks WS handshake state,
 not the TCP-level pagi.connection that the base class uses.

See PAGI::Context::WebSocket and PAGI::Context::SSE for the full method reference on each subclass.

EXTENSIBILITY

Override _type_map to add or replace protocol types:

package MyApp::Context;
our @ISA = ('PAGI::Context');

sub _type_map {
    my ($class) = @_;
    return {
        %{ $class->SUPER::_type_map },
        grpc => 'MyApp::Context::GRPC',
    };
}

Override _resolve_class for custom resolution logic beyond the type map.

CONSTRUCTOR

new

my $ctx = PAGI::Context->new($scope, $receive, $send);

Factory constructor. Returns a subclass instance based on $scope->{type}. Defaults to HTTP if type is missing or unknown.

CLASS METHODS

_type_map

my $map = PAGI::Context->_type_map;

Returns a hashref mapping scope type strings to subclass package names. Override in a subclass to add or replace protocol types.

_resolve_class

my $class = PAGI::Context->_resolve_class($scope);

Resolves the scope to a subclass package name. Looks up $scope->{type} in _type_map; defaults to the http mapping if the type is missing or unknown. Override for custom resolution logic.

METHODS

Scope Accessors

$ctx->scope;          # raw $scope hashref
$ctx->type;           # $scope->{type}
$ctx->path;           # $scope->{path}
$ctx->raw_path;       # $scope->{raw_path} // $scope->{path}
$ctx->query_string;   # $scope->{query_string} // ''
$ctx->scheme;         # $scope->{scheme} // 'http'
$ctx->client;         # $scope->{client}
$ctx->server;         # $scope->{server}
$ctx->headers;        # $scope->{headers} arrayref of [name, value]

Path Parameters

my $params = $ctx->path_params;           # hashref
my $id     = $ctx->path_param('id');      # strict: dies if missing
my $id     = $ctx->path_param('id', strict => 0);  # returns undef

path_params returns the $scope->{path_params} hashref (set by the router), defaulting to {} if not present.

path_param returns a single parameter by name. By default it dies if the key is not found (strict mode). Pass strict => 0 to return undef for missing keys instead.

Protocol Introspection

$ctx->is_http;        # true if type eq 'http'
$ctx->is_websocket;   # true if type eq 'websocket'
$ctx->is_sse;         # true if type eq 'sse'
my $value = $ctx->header('Content-Type');

Returns the last value for the named header (case-insensitive), or undef if not found.

receive

my $receive = $ctx->receive;

Returns the raw $receive coderef. Calling it returns a Future that resolves to the next protocol event hashref from the client.

# Read an HTTP request body event
my $event = await $ctx->receive->();
# $event = { type => 'http.request', body => '...' }

# Read a WebSocket message
my $msg = await $ctx->receive->();
# $msg = { type => 'websocket.receive', text => 'hello' }

Most users should prefer the protocol helpers ($ctx->request, $ctx->websocket, $ctx->sse) which handle the event protocol internally. Use receive only for raw protocol access.

send

my $send = $ctx->send;

Returns the raw $send coderef. Calling it with an event hashref returns a Future that resolves when the event has been sent.

# Send an HTTP response (two events: start + body)
await $ctx->send->({ type => 'http.response.start', status => 200,
                     headers => [['content-type', 'text/plain']] });
await $ctx->send->({ type => 'http.response.body', body => 'Hello' });

# Accept a WebSocket connection
await $ctx->send->({ type => 'websocket.accept' });

Most users should prefer the protocol helpers ($ctx->response, $ctx->websocket, $ctx->sse) which build and send events for you. Use send only for raw protocol access.

stash

my $stash = $ctx->stash;   # PAGI::Stash instance

Returns a PAGI::Stash wrapping $scope->{'pagi.stash'}. Lazy-constructed and cached.

session

my $session = $ctx->session;   # PAGI::Session instance

Returns a PAGI::Session wrapping $scope->{'pagi.session'}. Lazy-constructed and cached. Dies if session middleware has not run. Use has_session to check availability first.

has_session

if ($ctx->has_session) {
    my $user_id = $ctx->session->get('user_id');
}

Returns true if session middleware has populated $scope->{'pagi.session'}.

state

my $state = $ctx->state;   # hashref

Returns $scope->{state} - the app/endpoint-level shared state.

Connection State

$ctx->connection;           # PAGI::Server::ConnectionState object
$ctx->is_connected;         # boolean
$ctx->is_disconnected;      # boolean
$ctx->disconnect_reason;    # string or undef
$ctx->on_disconnect($cb);   # register callback

Delegates to $scope->{'pagi.connection'}.

EVENT DISPATCHER

The event dispatcher provides a generic, protocol-agnostic way to handle PAGI events. It is most useful when the receive stream carries a mix of protocol events and application-level events injected by middleware such as PAGI::Middleware::Channels.

my $ctx = PAGI::Context->new($scope, $receive, $send);

$ctx->on('websocket.receive', async sub {
    my ($ctx, $event) = @_;
    my $text = $event->{text} // '';
    await $ctx->send->({ type => 'websocket.send', text => "echo: $text" });
});

$ctx->on('chat.message', async sub {
    my ($ctx, $event) = @_;
    # handle a channel-injected event
});

$ctx->on_error(sub {
    my ($ctx, $error, $source) = @_;
    warn "[$source] $error";
});

my $reason = await $ctx->run;   # 'disconnect', 'stop', or 'error'

on

$ctx->on($event_type, $callback);   # returns $ctx

Register a handler for a raw PAGI event type string. Multiple handlers may be registered for the same type; they are called in registration order. Handlers receive ($ctx, $event). Handlers may be plain coderefs or async subs; if a handler returns a Future, run() awaits it before continuing.

Returns $ctx for chaining.

on_error

$ctx->on_error($callback);   # returns $ctx

Register an error callback. It is called when $receive->() fails ($source = 'receive') or when a registered handler throws ($source = 'handler'). Callbacks receive ($ctx, $error, $source).

Multiple callbacks may be registered and are called in order. Callbacks may be async subs; if a callback returns a Future, it is awaited. If no callbacks are registered, errors are emitted via warn.

Returns $ctx for chaining.

# Avoid circular references - weaken if the callback closes over $ctx
use Scalar::Util qw(weaken);
my $weak = $ctx;
weaken $weak;
$ctx->on_error(sub { my ($ctx, $err, $src) = @_; warn "[$src] $err" });

stop

$ctx->stop;   # returns $ctx

Signal the run() loop to exit cleanly after the current handler finishes. run() will resolve with reason 'stop'.

Returns $ctx for chaining.

run

my $reason = await $ctx->run;

Start the event dispatch loop. Reads events from the receive stream and dispatches each to registered handlers. The loop runs until one of:

  • The protocol's terminal disconnect event arrives (websocket.disconnect, sse.disconnect, http.disconnect) - resolves with 'disconnect'

  • stop() was called - resolves with 'stop'

  • $receive->() fails - fires on_error callbacks and resolves with 'error'

run() always resolves successfully (never rejects). The caller does not need to catch it.

Calling run() a second time while already running throws synchronously.

When run() resolves, all registered handlers and error callbacks are cleared to break closure-based reference cycles.

SEE ALSO

Protocol subclasses (full method reference for each protocol):

PAGI::Context::HTTP, PAGI::Context::WebSocket, PAGI::Context::SSE

Underlying protocol objects (standalone use, or direct access via $ctx->websocket, $ctx->sse, $ctx->request, $ctx->response):

PAGI::WebSocket, PAGI::SSE, PAGI::Request, PAGI::Response

Shared services:

PAGI::Stash, PAGI::Session

1 POD Error

The following errors were encountered while parsing the POD:

Around line 77:

Non-ASCII character seen before =encoding in '──────────────────'. Assuming UTF-8