NAME

PAGI::Server::ConnectionState - Connection state tracking for HTTP requests

SYNOPSIS

my $conn = $scope->{'pagi.connection'};

# Synchronous, non-destructive check
if ($conn->is_connected) {
    # Client still connected
}

# Get disconnect reason (undef while connected)
my $reason = $conn->disconnect_reason;

# Register cleanup callback
$conn->on_disconnect(sub {
    my ($reason) = @_;
    cleanup_resources();
});

# Await disconnect (if Future provided)
if (my $future = $conn->disconnect_future) {
    my $reason = await $future;
}

DESCRIPTION

PAGI::Server::ConnectionState provides a mechanism for applications to detect client disconnection without consuming messages from the receive queue.

This addresses a fundamental limitation in the PAGI (and ASGI) model where checking for disconnect via receive() may inadvertently consume request body data.

See PAGI Specification 0.3 for the full specification.

METHODS

new

my $conn = PAGI::Server::ConnectionState->new();
my $conn = PAGI::Server::ConnectionState->new(future => $disconnect_future);

Creates a new connection state object. The optional future argument provides a Future that will be resolved when disconnect occurs.

is_connected

my $connected = $conn->is_connected;  # Boolean

Returns true if the connection is still open, false if disconnected.

This is a synchronous, non-destructive check that does not consume messages from the receive queue.

disconnect_reason

my $reason = $conn->disconnect_reason;  # String or undef

Returns the disconnect reason string, or undef if still connected.

Standard reason strings:

  • client_closed - Client initiated clean close (TCP FIN)

  • client_timeout - Client stopped responding (read timeout)

  • idle_timeout - Connection idle too long between requests

  • write_timeout - Response write timed out

  • write_error - Socket write failed (EPIPE, ECONNRESET)

  • read_error - Socket read failed

  • protocol_error - HTTP parse error, invalid request

  • server_shutdown - Server shutting down gracefully

  • body_too_large - Request body exceeded limit

disconnect_future

my $future = $conn->disconnect_future;  # Future or undef
my $reason = await $future;

Returns a Future that resolves when the connection closes, or undef if the server does not support this feature.

The Future resolves with the disconnect reason string.

This is useful for racing against other async operations:

await Future->wait_any($disconnect_future, $event_future);

on_disconnect

$conn->on_disconnect(sub {
    my ($reason) = @_;
    # cleanup code
});

Registers a callback to be invoked when disconnect occurs.

  • May be called multiple times to register multiple callbacks

  • Callbacks are invoked in registration order

  • Callbacks receive the disconnect reason as the first argument

  • If registered after disconnect already occurred, callback is invoked immediately with the reason

  • One callback's failure does not prevent other callbacks from being invoked

_mark_disconnected

$conn->_mark_disconnected($reason);

Internal method - Called by the server when disconnect is detected.

Updates the connection state and invokes all registered callbacks. Applications should not call this method directly.

State transitions occur in this order:

1. is_connected() returns false
2. disconnect_reason() returns the reason string
3. disconnect_future() resolves with the reason (if provided)
4. on_disconnect callbacks are invoked in registration order

USAGE WITH PAGI::Request

The PAGI::Request class provides convenience methods that delegate to the connection object:

my $req = PAGI::Request->new($scope, $receive);

# Access connection object directly
my $conn = $req->connection;

# Convenience delegates
$req->is_connected;                    # $conn->is_connected
$req->is_disconnected;                 # !$conn->is_connected
$req->disconnect_reason;               # $conn->disconnect_reason
$req->on_disconnect(sub { ... });      # $conn->on_disconnect(...)
$req->disconnect_future;               # $conn->disconnect_future

EXAMPLE: Basic Connection Check

async sub handler {
    my ($scope, $receive, $send) = @_;
    my $conn = $scope->{'pagi.connection'};

    # Check before expensive work
    return unless $conn->is_connected;

    my $result = await expensive_operation();

    # Check again before responding
    return unless $conn->is_connected;

    await $send->({ type => 'http.response.start', status => 200, headers => [] });
    await $send->({ type => 'http.response.body', body => $result, more => 0 });
}

EXAMPLE: Cleanup on Disconnect

async sub handler {
    my ($scope, $receive, $send) = @_;
    my $conn = $scope->{'pagi.connection'};

    my $temp_file = create_temp_file();
    my $lock = acquire_lock();

    # Register cleanup - runs automatically if client disconnects
    $conn->on_disconnect(sub {
        my ($reason) = @_;
        $temp_file->unlink;
        $lock->release;
        log_info("Client disconnected: $reason");
    });

    # Do work - cleanup happens automatically if client leaves
    my $result = await process_data($temp_file);

    # Normal cleanup on success
    $temp_file->unlink;
    $lock->release;

    await send_response($send, $result);
}

SEE ALSO

PAGI::Request - High-level request API with connection convenience methods

PAGI::Server - Reference server implementation

PAGI::Server::Connection - Per-connection state machine (internal)