=encoding UTF-8

=head1 NAME

PAGI::Spec::Www - PAGI message formats for HTTP, WebSocket, and SSE

=head1 HTTP, WebSocket & SSE PAGI Message Format

B<Version>: 0.3 (Draft)

The HTTP, WebSocket & SSE PAGI sub-specification defines how HTTP/1.1, HTTP/2, WebSocket, and Server-Sent Events (SSE) connections are transported within PAGI.

It is designed to be a superset of the PSGI specification and specifies how to translate between PAGI and PSGI for compatible requests.

=head2 Spec Versions

=over

=item -

C<0.3>: Added C<pagi.transport> outbound flow-control introspection
(C<buffered_amount>, high/low watermarks, C<on_high_water>/C<on_drain> callbacks)
and the C<websocket.http.response> WebSocket denial-response extension. Reworked
disconnect handling into one abnormal-only reason vocabulary, adding
C<on_complete> (successful completion) alongside C<on_disconnect> (abnormal
drops) on C<pagi.connection>. Documented the server-supplied HTTP, SSE, and
WebSocket behaviors and WebSocket over HTTP/2 (RFC 8441). B<Removed>
C<pagi.features> (superseded by C<extensions>) and the per-send C<timeout> field
(superseded by transport backpressure and keepalive timeouts).

=item -

C<0.2>: Added keepalive events (C<websocket.keepalive>, C<sse.keepalive>),
disconnect reasons, SSE support for all HTTP methods (not just GET) with the
C<sse.request> receive event, and the C<pagi.connection> scope key for
non-destructive disconnect detection.

=item -

C<0.1>: Initial draft, based on ASGI 2.5, including Server-Sent Events support.

=back

C<< $scope->{pagi}{spec_version} >> reports which version of this specification
governs an HTTP, WebSocket, or SSE connection. It is optional; if omitted,
applications MUST assume C<'0.1'>. Omitting it declares support for version 0.1
only, so the server MUST NOT use behaviour introduced by a later version on
that connection. The core PAGI C<version> does not affect this default.

=head2 Common Data Types

=head3 Headers Format

Headers are represented as C<ArrayRef[ArrayRef[Bytes]]> - an array of 2-element tuples where each tuple contains C<[name, value]>:

    headers => [
        ['content-type', 'text/html; charset=utf-8'],
        ['x-request-id', '12345'],
        ['set-cookie', 'session=abc'],
        ['set-cookie', 'tracking=xyz'],  # duplicate names allowed
    ]

B<Why tuples instead of PSGI's flat array?>

PSGI uses a flat array with implicit pairs: C<['Content-Type', 'text/html', 'X-Custom', 'value']>. PAGI uses explicit tuples for several reasons:

=over 4

=item 1.

B<Clearer iteration> - Each header is a discrete unit:

    # PAGI - straightforward
    for my $header (@$headers) {
        my ($name, $value) = @$header;
    }

    # PSGI - requires index math
    for (my $i = 0; $i < @$headers; $i += 2) {
        my ($name, $value) = @{$headers}[$i, $i+1];
    }

=item 2.

B<Explicit duplicates> - Duplicate header names (common for C<Set-Cookie>) are visually obvious

=item 3.

B<Easier manipulation> - Filtering, mapping, and transforming headers works naturally with array operations:

    # Remove all cookies
    my @filtered = grep { $_->[0] ne 'set-cookie' } @$headers;

    # Find a header
    my ($ct) = grep { $_->[0] eq 'content-type' } @$headers;

=item 4.

B<ASGI compatibility> - Matches the Python ASGI specification that PAGI is modeled on

=back

B<Rules:>

=over

=item -

Header names MUST be lowercase byte strings

=item -

Header values MUST be byte strings (opaque, not decoded)

=item -

Each inner arrayref MUST contain exactly 2 elements: C<[name, value]>

=item -

Duplicate header names are permitted (required for C<Set-Cookie>, etc.)

=back

B<HTTP::Headers Compatibility:>

PSGI's flat array format is more compatible with C<<< HTTP::Headers->flatten() >>>. If you need to interoperate:

    # PAGI tuples -> flat array (for HTTP::Headers)
    my @flat = map { @$_ } @$pagi_headers;
    my $hh = HTTP::Headers->new(@flat);

    # Flat array -> PAGI tuples
    my @flat = $http_headers->flatten;
    my @pagi = map { [$flat[$_*2], $flat[$_*2+1]] } 0 .. ($#flat/2);

=head3 Scope Extension Keys

The scope hashref may contain additional keys beyond those defined in the HTTP, WebSocket, and SSE sections below. This allows middleware and applications to pass data through the request lifecycle.

B<Reserved prefixes:>

=over

=item -

C<pagi.*> - Reserved for PAGI spec extensions (e.g., C<pagi.router>, C<pagi.session>)

=item -

Keys without a dot prefix (e.g., C<type>, C<method>, C<path>) - Reserved for core spec use

=back

B<Custom keys:>

Applications and third-party middleware SHOULD use a unique key or prefix to avoid collisions. Two common patterns:

    # Pattern 1: Single hashref (recommended for grouped data)
    $scope->{myauth} = {
        user  => $user_object,
        roles => ['admin', 'editor'],
    };

    # Pattern 2: Dotted keys (for flat/independent values)
    $scope->{'myapp.request_id'} = $uuid;
    $scope->{'myapp.started_at'} = time();

Either approach works - choose based on whether your data is naturally grouped or independent.

B<Allowed values:>

Scope values may be any Perl data type:

=over

=item -

Scalars, arrayrefs, hashrefs

=item -

Blessed objects

=item -

Code references

=back

B<Note:> Objects in scope are NOT serializable. Do not assume scope can be passed between processes, persisted to storage, or serialized to JSON. Scope exists only for the lifetime of a single request within a single process.

B<Example:>

    # Authentication middleware - hashref pattern
    $scope->{myauth} = {
        user  => $user_object,
        roles => ['admin', 'editor'],
        authenticated_at => time(),
    };

    # Router middleware (PAGI built-in)
    $scope->{'pagi.router'} = {
        params => { id => '42' },
        route  => '/users/:id',
    };

    # Application accessing middleware data
    my $user = $scope->{myauth}{user};
    my $id = $scope->{'pagi.router'}{params}{id};

B<Middleware guidelines:>

=over

=item -

Document the keys your middleware adds to scope

=item -

Use consistent naming within your namespace

=item -

Don't modify keys outside your namespace

=item -

Check for key existence before assuming middleware ran

=back

=head2 HTTP

PAGI covers HTTP/1.0, HTTP/1.1, and HTTP/2. Protocol servers assign separate scopes for requests within the same HTTP/2 connection and multiplex responses appropriately.

=head3 HTTP/2 Stream Mapping

PAGI servers must translate HTTP/2 frames into PAGI HTTP events per stream. Applications only see structured events, not raw frames:

=over

=item -

B<HEADERS>: start a new PAGI C<http> scope and emit an initial C<http.request> event with headers and C<<< more => 1 >>> if DATA will follow, or C<<< more => 0 >>> if C<END_STREAM> was signaled immediately.

=item -

B<DATA>: emit subsequent C<http.request> events with C<<< body => <chunk> >>> and C<<< more => 1 >>> or C<0> depending on C<END_STREAM>.

=item -

B<END_STREAM>: if no DATA frames, send an C<http.request> with C<<< body => '' >>> and C<<< more => 0 >>> to signal end of request.

=item -

B<RST_STREAM>: trigger a C<http.disconnect> event and cancel any outstanding Futures for that scope.

=item -

B<WINDOW_UPDATE / PRIORITY>: ignored by default (advanced flow control is optional).

=item -

B<PUSH_PROMISE>: not supported; servers must reject push promises.

=back

Only HTTP/2 over TLS (C<h2>) is required for the initial implementation; cleartext HTTP/2 (C<h2c>) is optional.

The HTTP version is available in the scope. Pseudo headers (like C<:authority>) from HTTP/2 must be removed; if C<:authority> is present, its value must be used to populate or override the C<host> header.

Multiple C<Set-Cookie> headers must be preserved individually, and C<Cookie> headers should be combined or split according to the version-specific rules (as per RFC 7230, RFC 6265, and RFC 9113).

=head3 Cookie Header Normalization

PAGI servers B<must normalize C<Cookie> headers> before passing them to the application.

=over

=item -

If multiple C<Cookie:> headers are received from the client (which may happen in real-world deployments despite RFC guidance), the server must:

=item -

Concatenate them using C<"; "> (semicolon followed by space)

=item -

Ensure only B<one C<cookie> header> appears in the PAGI C<headers> list

=back

B<Example:>

If the client sends:

    Cookie: a=1 Cookie: b=2; c=3

The PAGI scope must include:

    headers => [
      [ 'cookie', 'a=1; b=2; c=3' ]
    ]

The server does not parse the cookie string into key-value pairs -- parsing is left to middleware or application code. The server only guarantees RFC-compliant normalization.

=head3 HTTP Connection Scope

Each HTTP request has a single-request connection scope. Scope keys:

=over

=item -

C<type> (String) -- C<"http">

=item -

C<< $scope->{pagi}{version} >> (String) -- The PAGI core spec version (e.g. C<'0.4'>)

=item -

C<< $scope->{pagi}{spec_version} >> (String) -- The version of this HTTP sub-specification (e.g. C<'0.3'>). Optional; if omitted, assume C<'0.1'>.

=item -

C<http_version> (String) -- C<"1.0">, C<"1.1">, or C<"2">

=item -

C<method> (String) -- Uppercase HTTP method

=item -

C<scheme> (String, default C<"http">) -- URL scheme (C<"http"> or C<"https">)

=item -

C<path> (String) -- Decoded HTTP path

=item -

C<raw_path> (Bytes, optional) -- Original HTTP path bytes

=item -

C<query_string> (Bytes) -- Percent-encoded query string

=item -

C<root_path> (String, default C<"">) -- Application mount path, equivalent to C<SCRIPT_NAME> in PSGI

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]]) -- Original HTTP headers. Header names B<must> be lower-cased byte strings and header values must be opaque byte strings.

=item -

C<client> (ArrayRef[String, Int], optional) -- C<[host, port]> of client

=item -

C<server> (ArrayRef[String, Optional[Int]], optional) -- C<[host, port]> or C<[path, undef]> for Unix sockets

=item -

C<state> (HashRef, optional) -- A shallow copy of the lifespan C<state> namespace. Top-level keys are private to this request; values (object references) are shared. See L<PAGI::Spec::Lifespan/Lifespan State> for how to share mutable data safely.

=item -

C<pagi.connection> (Object) -- Per-request connection-state object for disconnect detection and response progress (C<response_started> / C<response_complete>). See L</Connection State> below.

=item -

C<extensions> (HashRef, default C<{}>) -- Optional server capabilities advertised for this connection (e.g. C<tls>). Each present key names an extension mapping to a hashref of extension data; applications check for a key before relying on it. See L<PAGI::Spec::Extensions>.

=back

=head3 Request - C<receive> event

Note: Chunked transfer encoding must be de-chunked by the server. Each C<http.request> represents a de-chunked body fragment.

Keys:

=over

=item -

C<type> -- C<"http.request">

=item -

C<body> (Bytes, default C<"">) -- Request body chunk

=item -

C<more> (Int, default C<0>) -- C<1> if more body data is forthcoming, otherwise C<0>

=back

=head3 Response Start - C<send> event

Note: Protocol servers are NOT required to flush on C<http.response.start>, giving flexibility to emit an error response in case of internal application errors before data is sent.

Transfer-Encoding headers sent by the application must be ignored. Content-Encoding (e.g. gzip) is under application control.

Keys:

=over

=item -

C<type> -- C<"http.response.start">

=item -

C<status> (Int) -- HTTP status code

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]], default C<[]>) -- Response headers

=item -

C<trailers> (Int, default C<0>) -- C<1> if trailers will be sent after body via C<http.response.trailers>, otherwise C<0>

=back

B<Header byte safety.> Because header names and values cross into HTTP message
framing, a server B<MUST NOT> emit a header whose name or value contains C<CR>
(C<\x0D>), C<LF> (C<\x0A>), or C<NUL> (C<\x00>), nor a name containing any other
control character. The server B<MUST> reject such a header by failing the Future
returned by C<$send> rather than forwarding it or silently rewriting the
offending bytes: letting C<CR>/C<LF> through permits HTTP response splitting
(CRLF injection), while silently stripping them mutates the application's data
without signalling the bug. This is the same guarantee C<sse.send> gives for
its C<event> and C<id> fields (see L</Send SSE - C<send> event>): the server's
emission path is the single point that guards the wire, so applications and
frameworks MAY hold header values as opaque bytes and need not pre-validate
them. (RFC 9110 S5.5; RFC 9112 S11.1.)

=head3 Response Body - C<send> event

Keys:

=over

=item -

C<type> -- C<"http.response.body">

=item -

C<body> (Bytes, default C<"">) -- Response body chunk

=item -

C<file> (String) -- Absolute path to file for server to open and stream

=item -

C<fh> (Filehandle) -- Already-open filehandle for server to stream

=item -

C<offset> (Int, default C<0>) -- Byte offset to start reading from (for range requests)

=item -

C<length> (Int, optional) -- Number of bytes to send (omit to read until EOF)

=item -

C<more> (Int, default C<0>) -- Indicates more body content to follow (C<1> if true, otherwise C<0>). B<Ignored for C<file> and C<fh> responses> which are implicitly complete.

=back

The C<body>, C<file>, and C<fh> keys are B<mutually exclusive> - exactly one MUST be provided per event. Applications B<MUST> provide C<body> as encoded bytes. For text content, this typically means UTF-8 encoding before sending. The C<Content-Length> header (if present) MUST reflect byte length, not character length.

B<Note:> When using C<file> or C<fh>, the response is implicitly complete after the file/handle contents are sent. The C<more> key is ignored for these response types - there is no need to specify C<<< more => 0 >>>.

When C<file> or C<fh> is provided, servers MUST stream the file contents efficiently:

=over

=item -

Servers SHOULD stream large files in chunks to avoid memory bloat

=item -

Servers MAY use zero-copy mechanisms (sendfile, splice) when appropriate

=item -

The C<offset> and C<length> keys enable range request support (e.g., HTTP 206 Partial Content)

=item -

For production file serving, consider using XSendfile middleware to delegate to a reverse proxy

=item -

When using C<file>, the server opens the file, streams it, and closes it

=item -

When using C<fh>, the application retains ownership and B<MUST> close the handle after the C<<< $send->() >>> Future completes

=back

B<Error Handling:>

=over

=item -

If C<file> cannot be opened (not found, permission denied), the C<<< $send->() >>> Future MUST fail with an appropriate exception

=item -

If C<fh> is invalid or closed, the C<<< $send->() >>> Future MUST fail immediately

=item -

Applications SHOULD validate file existence before sending C<http.response.start> to avoid incomplete responses

=back

B<Validation:>

=over

=item -

C<offset> MUST be a non-negative integer

=item -

C<length> MUST be a non-negative integer if provided

=item -

If C<offset> exceeds file size, servers SHOULD send zero bytes

=back

B<Examples:>

    # Full file streaming
    await $send->({
        type => 'http.response.body',
        file => '/var/www/static/large-video.mp4',
    });

    # Range request (bytes 1000-1999)
    await $send->({
        type => 'http.response.body',
        file => '/var/www/static/document.pdf',
        offset => 1000,
        length => 1000,
    });

    # Streaming from already-open filehandle
    open my $fh, '<:raw', '/tmp/generated-report.csv' or die $!;
    await $send->({
        type => 'http.response.body',
        fh => $fh,
    });
    close $fh;  # Application MUST close after send Future completes

=head3 Response Trailers - C<send> event

Only valid when C<http.response.start> was sent with C<<< trailers => 1 >>>. After trailers are transmitted the server MUST consider the response body complete.

Keys:

=over

=item -

C<type> -- C<"http.response.trailers">

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]], default C<[]>) -- Trailer headers encoded the same way as response headers (lower-case names, byte values)

=back

=head3 Application Produced No Response

If an HTTP application's Future resolves without any C<http.response.start> having
been sent, and the client is B<still connected>, the client is left waiting for a
response that will never come. As a last-resort B<backstop> the server produces a
C<500> response, logs it, and closes the connection so the client is not left
hanging.

This is a backstop, B<not> a completion policy. Which terminal response to produce
-- a C<404>, a C<204>, an error page, or anything else -- is the application's or
framework's responsibility, and a well-behaved application always produces one
before its Future resolves. PAGI exposes only the observer-independent fact that
no response was started -- read C<< $conn->response_started >> on the
C<pagi.connection> object; it makes B<no> judgment about whether a response is
"complete".
An application that does not handle the C<http> scope may decline by raising an
exception, which the server treats identically -- the same C<500> backstop.

If the client has B<already disconnected> (see L</Disconnect - C<receive> event>),
this is not an application error: the server MUST NOT synthesize a C<500> and MUST
NOT log an error.

=head3 Server-Supplied Headers and Behaviors

Beyond the headers an application sets on C<http.response.start>, a conforming
server supplies a few headers and framing details on its own, and may originate
certain responses without invoking the application. Applications and frameworks
SHOULD be aware of these so they neither duplicate nor fight them.

=over

=item -

B<C<Date>> -- The server SHOULD add a C<Date> response header in HTTP-date format
(RFC 7231) when the application did not supply one. (The reference server adds it
to both HTTP/1.1 and HTTP/2 responses.)

=item -

B<Body framing> -- When the application provides no C<Content-Length> on an
HTTP/1.1 response, the server frames the body with C<Transfer-Encoding: chunked>.
Applications SHOULD set C<Content-Length> (byte length) when the size is known.
HTTP/2 uses its own DATA-frame framing, where neither header applies.

=item -

B<C<Connection> (HTTP/1.0)> -- For HTTP/1.0 clients the server manages the
C<Connection> header: C<close> when the response carries no C<Content-Length>
(the close delimits the body), or C<keep-alive> when the client requested
keep-alive and a C<Content-Length> is present. Applications SHOULD NOT set
C<Connection> themselves.

=item -

B<C<100 Continue>> -- When an HTTP/1.1 client sends C<Expect: 100-continue>, the
server sends an interim C<100 Continue> before the application first reads the
request body, then delivers the body through C<receive> as usual. This is
transparent to the application. HTTP/2 has no C<Expect: 100-continue> mechanism.

=back

B<Server-generated responses.> A server MAY originate a response on its own --
before the application runs, or in place of it -- for protocol and resource
errors it detects, each carrying a short C<text/plain> body. Applications cannot
intercept these (the request never reaches the application, or the application
has already failed). A conforming server is expected to generate at least:

=over

=item -

C<400 Bad Request> -- a malformed request line, headers, or message framing.

=item -

C<413 Payload Too Large> -- the request body, or its declared C<Content-Length>,
exceeds the server's configured C<max_body_size>.

=item -

C<414 URI Too Long> / C<431 Request Header Fields Too Large> -- the request line
or header section exceeds the server's limits.

=item -

C<500 Internal Server Error> -- the application threw before sending
C<http.response.start>. (Once the response has started the server can only
truncate the connection -- see L</Disconnected Client - sending after disconnect>.)

=item -

C<501 Not Implemented> -- an unsupported request, e.g. an unrecognized
C<Transfer-Encoding>, or an Extended C<CONNECT> without a supported C<:protocol>.

=back

The exact reason phrases and body text are server-defined; applications MUST NOT
depend on them.

=head3 Disconnected Client - sending after disconnect

If the client disconnects or cancels the connection, servers MUST send an explicit C<http.disconnect> event to the application and update the connection state (see L</Connection State>).

A subsequent C<$send> is a B<no-op>: it neither delivers data nor raises. Applications therefore detect disconnection through the C<http.disconnect> event or the C<pagi.connection> connection-state object (see L</Connection State>) -- PAGI's non-destructive, race-friendly mechanism for exactly this -- rather than by inspecting the result of C<$send>.

Applications MUST gracefully handle disconnect events by:

=over

=item -

Immediately halting unnecessary processing upon disconnect

=item -

Optionally sending minimal final acknowledgment messages

=item -

Executing asynchronous cleanup of resources as necessary.

=back

=head3 Disconnect - C<receive> event

Sent to the application if receive is called after a response has been sent or after the HTTP connection has been closed.

Keys:

=over

=item -

C<type> -- C<"http.disconnect">

=back

=head3 Connection State

The C<pagi.connection> scope key provides a mechanism for applications to detect client disconnection B<without consuming messages from the receive queue>. This addresses a fundamental limitation where checking for disconnect via C<receive()> may inadvertently consume request body data.

=head4 The Problem

In PAGI's pull-based receive model, the only way to know if a client has disconnected is to consume the next message:

    my $message = await $receive->();
    if ($message->{type} eq 'http.disconnect') {
        # Client is gone - but if it wasn't a disconnect, that data is lost!
    }

The connection state object solves this with synchronous, non-destructive checks.

=head4 Connection Object Interface

The object reports two distinct ways a request can end, through B<separate>
callbacks, so an application never has to disambiguate them:

=over 4

=item *

An B<abnormal disconnect> -- the client goes away, a timeout fires, or an error
occurs -- B<while the application is still working>.

=item *

The request B<completing successfully> -- the response was fully delivered.

=back

B<When a server provides C<on_complete>>, exactly one of C<on_disconnect> and C<on_complete> fires for a given request.

Servers MUST provide:

B<C<is_connected()>> - Returns true while the connection is open, false once it has closed (for any reason).

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

B<C<disconnect_reason()>> - Returns the abnormal-disconnect reason string (see L</Standard Disconnect Reasons>), or C<undef> if the connection is still open or completed normally.

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

B<C<on_disconnect($callback)>> - Registers a callback invoked B<only on abnormal disconnect> (client gone, timeout, error) -- not on normal completion. The callback receives the disconnect reason.

    $conn->on_disconnect(sub {
        my ($reason) = @_;
        cancel_work();          # the client is gone; stop and clean up
    });

=over

=item -

May be called multiple times to register multiple callbacks

=item -

Callbacks are invoked in registration order

=item -

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

=back

B<C<response_started()>> - Returns true once B<this request's> response has started
-- that is, once C<http.response.start> has been emitted for this scope -- and false
before. The server sets it when it processes the response-start event, so it is true no
matter who produced the response: the application, a framework, a middleware that
short-circuited the request, or a B<server-synthesized error/backstop response>
(e.g. the C<500> the server emits when the application produces nothing -- see
L</Application Produced No Response>). Read-only to the application.

C<response_started> does not identify which producer started the response; an
application response and a server-synthesized backstop set it alike. The
clean-versus-abnormal outcome is reported separately by the request's terminal
signal -- C<on_complete> on a clean finish, C<on_disconnect> with
C<disconnect_reason> C<'server_error'> on a server backstop.

    my $started = $conn->response_started;  # Boolean, synchronous

A streaming response counts as started for its whole lifetime: C<response_started>
becomes true at C<http.response.start> and stays true while body chunks flow. Use
C<response_complete> to learn when the body has actually finished.

Servers SHOULD also provide:

B<C<on_complete($callback)>> - Registers a callback invoked B<only when the request completes successfully> (the response was fully delivered without the client disconnecting). The counterpart to C<on_disconnect>.

    $conn->on_complete(sub {
        commit_transaction();   # finished cleanly
    });

B<C<response_complete()>> - Returns true once B<this request's> response body has
been fully sent -- the terminal C<http.response.body> with C<<< more => 0 >>>, a
C<file>/C<fh> response, or the final trailers -- false while a response is still
streaming or has not started, and B<C<undef> if the server does not track
completion>. The synchronous counterpart to C<on_complete>, and read-only to the
application. As with C<disconnect_future>, test C<defined> to learn whether the
feature is available before relying on it.

    my $done = $conn->response_complete;   # undef = unsupported, else 0 or 1

This accessor is B<SHOULD>, not MUST, on purpose. "The body has been fully sent"
is not always a crisp instant: buffering, HTTP/2 flow-control windows, and
intermediaries can blur exactly when the last byte reaches the client, so some
servers cannot report completion precisely. The load-bearing fact -- whether a
response B<started> -- is the MUST (C<response_started>); completion is a
convenience layered on top, which is also why it shares the SHOULD level of its
callback form C<on_complete>. A future revision may promote both to MUST if
implementation experience shows the timing can be pinned down reliably across
transports.

B<C<disconnect_future()>> - Returns a Future that resolves, with the reason, on abnormal disconnect -- useful for racing against long-running work, e.g. C<< Future->wait_any($work, $conn->disconnect_future) >>. Returns C<undef> if not supported.

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

=head4 What "connection" means here

The object is named for the connection, but it is scoped to a B<single request>:
each request gets its own C<pagi.connection>, and the C<response_started> /
C<response_complete> flags describe B<that request's> response, never the transport
as a whole. This matters the moment a transport carries more than one request:

=over 4

=item *

B<HTTP/1.1> reuses a connection for requests B<sequentially> (keep-alive); each
request is a fresh scope with its own connection object.

=item *

B<HTTP/2> B<multiplexes> many requests over one TCP connection as independent
streams. PAGI maps each stream to its own C<http> scope (see L</HTTP/2 Stream
Mapping>), so ten concurrent streams have ten connection objects and ten
independent C<response_started> flags -- one request starting its response is never
visible as another's.

=item *

B<HTTP/3> (when supported) multiplexes streams over QUIC the same way; the
per-request scoping is identical, so nothing here changes.

=back

The C<response_started> / C<response_complete> flags are defined for the C<http>
scope only. B<SSE> is treated as its own scope type (C<< type => 'sse' >>) even
though it is really a long-lived response over HTTP: it begins with C<sse.start>
rather than C<http.response.start>, so these HTTP flags do not apply to it. An
SSE-specific equivalent may be defined later if a need arises.

=head4 Standard Disconnect Reasons

Every reason below describes an B<abnormal> end to the request -- the connection
went away, a timeout fired, or the server aborted the exchange before the
response was fully delivered. These are the values reported by
C<disconnect_reason()>, passed to C<on_disconnect> callbacks, and resolved from
C<disconnect_future()>. A request that completes successfully has B<no> reason
(C<disconnect_reason()> returns C<undef> and C<on_complete> fires instead).

Servers MUST use these standard reason strings where the condition applies:

=over 4

=item C<client_closed>

Client initiated clean close (TCP FIN) mid-request

=item C<client_timeout>

Client stopped responding (read timeout)

=item C<idle_timeout>

Connection idle too long before the request arrived

=item C<keepalive_timeout>

Keep-alive connection idled out between requests

=item C<write_timeout>

Response write timed out

=item C<write_error>

Socket write failed (EPIPE, ECONNRESET)

=item C<read_error>

Socket read failed

=item C<protocol_error>

HTTP parse error, invalid request

=item C<server_shutdown>

Server shutting down gracefully

=item C<server_error>

Unhandled server-side error aborted the request

=item C<body_too_large>

Request body exceeded limit

=item C<queue_overflow>

A bounded server queue (e.g. outgoing frames or buffered events) overflowed and the connection was dropped to relieve backpressure

=back

Servers MAY define additional reasons prefixed with C<x-> (e.g., C<x-rate-limited>).

=head4 Server Requirements

=over

=item 1.

MUST provide C<pagi.connection> in scope for C<http> type requests

=item 2.

MUST implement C<is_connected()>, C<disconnect_reason()>, and C<on_disconnect()> methods

=item 3.

SHOULD implement C<on_complete()> and C<disconnect_future()>

=item 4.

MUST fire C<on_disconnect> callbacks B<only> on abnormal disconnect, and C<on_complete> callbacks B<only> on successful completion -- never both for the same request

=item 5.

MUST update connection state as soon as disconnect is detected

=item 6.

MUST use standard reason strings where applicable

=item 7.

MUST NOT transition C<is_connected()> back to true once false (one-way transition)

=back

=head4 State Transition Order

When an B<abnormal disconnect> is detected, servers MUST update state in this order:

=over

=item 1.

Set C<is_connected()> to return false

=item 2.

Set C<disconnect_reason()> to return the reason string

=item 3.

Resolve C<disconnect_future()> with the reason (if provided)

=item 4.

Invoke C<on_disconnect> callbacks in registration order

=item 5.

Send C<http.disconnect> message to receive queue

=back

When a request B<completes successfully>, servers MUST invoke C<on_complete>
callbacks in registration order and MUST leave C<disconnect_reason()> as
C<undef>. C<on_disconnect> callbacks and C<disconnect_future()> MUST NOT fire on
this path.

=head4 Applicability

=over 4

=item C<http>

MUST provide C<pagi.connection>

=item C<websocket>

NOT APPLICABLE (use C<websocket.disconnect> events)

=item C<sse>

NOT APPLICABLE (use C<sse.disconnect> events)

=back

WebSocket and SSE have dedicated disconnect events and typically use handler objects (like C<PAGI::WebSocket>, C<PAGI::SSE>) that already manage connection state.

=head4 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 });
    }

=head4 Example: Cleanup on Disconnect vs. Completion

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

        my $temp_file = create_temp_file();

        # Abnormal end: client vanished, or a timeout/error fired mid-request.
        $conn->on_disconnect(sub {
            my ($reason) = @_;
            $temp_file->unlink;
            log_info("Aborted, client gone: $reason");
        });

        # Clean end: the response was fully delivered.
        $conn->on_complete(sub {
            $temp_file->unlink;
            log_info("Delivered OK");
        });

        my $result = await process_data($temp_file);
        await send_response($send, $result);
    }

Exactly one of the two callbacks runs, so the temp file is always removed
exactly once -- no manual cleanup in the request body, and no risk of a
double-unlink.

=head4 Example: Racing Against Disconnect

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

        my $disconnect_future = $conn->disconnect_future;
        my $event_future = wait_for_event();

        if ($disconnect_future) {
            # Race: wait for event OR disconnect
            await Future->wait_any($disconnect_future, $event_future);

            return unless $conn->is_connected;
        }

        my $event = $event_future->get;
        await send_response($send, $event);
    }

=head2 WebSocket

WebSocket servers handle fragmentation and PING/PONG messages. Servers MUST wait for a reply to C<websocket.connect> before completing the handshake. If C<websocket.close> is sent instead of C<websocket.accept>, the server MUST reject the connection with HTTP 403.

=head3 WebSocket Connection Scope

=over

=item -

C<type> (String) -- C<"websocket">

=item -

C<< $scope->{pagi}{version} >> (String) -- The PAGI core spec version (e.g. C<'0.4'>)

=item -

C<< $scope->{pagi}{spec_version} >> (String) -- The version of this WebSocket sub-specification (e.g. C<'0.3'>). Optional; if omitted, assume C<'0.1'>.

=item -

C<http_version> (String, default C<"1.1">) -- HTTP version used for handshake

=item -

C<scheme> (String, default C<"ws">) -- URL scheme (C<"ws"> or C<"wss">)

=item -

C<path> (String) -- Decoded path string

=item -

C<raw_path> (Bytes, optional) -- Original path bytes from request

=item -

C<query_string> (Bytes) -- Percent-encoded query string

=item -

C<root_path> (String, default C<"">) -- Mount point for application

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]]) -- Original headers

=item -

C<client> (ArrayRef[String, Int], optional)

=item -

C<server> (ArrayRef[String, Optional[Int]], optional)

=item -

C<subprotocols> (ArrayRef[String], default C<[]>)

=item -

C<state> (HashRef, optional)

=item -

C<extensions> (HashRef, default C<{}>) -- Optional server capabilities advertised for this connection (e.g. C<tls>, C<websocket.http.response>). See L<PAGI::Spec::Extensions>.

=back

=head4 Handshake Headers and Subprotocols

The C<headers> arrayref B<must> include all WebSocket handshake headers as raw byte strings, lower-cased, for example:

=over

=item -

C<upgrade>, C<connection>, C<sec-websocket-key>, C<sec-websocket-version>, C<host>, etc.

=item -

C<sec-websocket-protocol> (if present)

=back

The C<subprotocols> key is an arrayref of strings parsed from the C<Sec-WebSocket-Protocol> header by splitting on commas and trimming whitespace. If the header is absent, C<subprotocols> MUST be an empty arrayref.

=head3 WebSocket Events

=head4 Connect - C<receive> event

=over

=item -

C<type> -- C<"websocket.connect">

=back

=head4 Accept - C<send> event

=over

=item -

C<type> -- C<"websocket.accept">

=item -

C<subprotocol> (String, optional)

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]], optional)

=back

=head4 Receive - C<receive> event

=over

=item -

C<type> -- C<"websocket.receive">

=item -

C<bytes> (Bytes, optional)

=item -

C<text> (String, optional)

=back

Exactly one must be non-null.

The server B<must> UTF-8 decode incoming text frames into Unicode characters
for C<text>, and UTF-8 encode outgoing C<text> values to wire format. Binary
frames pass through as raw C<bytes> without encoding transformation.

If a text frame contains invalid UTF-8, the server B<must> fail the WebSocket
connection with close code 1007 (Invalid frame payload data) per RFC 6455.

=head4 Send - C<send> event

=over

=item -

C<type> -- C<"websocket.send">

=item -

C<bytes> (Bytes, optional)

=item -

C<text> (String, optional)

=back

Exactly one of C<bytes> or C<text> must be non-null.

=head4 Keepalive - C<send> event

Enables WebSocket protocol-level ping/pong keepalive. The server sends ping frames (opcode 0x9) at the specified interval. Clients automatically respond with pong frames per RFC 6455 - no application code required.

=over

=item -

C<type> -- C<"websocket.keepalive">

=item -

C<interval> (Number) -- Seconds between ping frames. C<0> disables keepalive.

=item -

C<timeout> (Number, optional) -- Seconds to wait for pong response. If no pong is received within this time, the connection is closed with code 1006 and the application receives a C<websocket.disconnect> event with C<<< reason => 'keepalive_timeout' >>>.

=back

B<Behavior:>

=over

=item -

Multiple C<websocket.keepalive> events update settings (last wins)

=item -

Omitting C<timeout> enables keepalive without dead connection detection (useful for high-latency connections)

=item -

Setting C<<< interval => 0 >>> stops the keepalive timer

=back

B<Example:>

    # Enable keepalive with 30s ping interval, 20s pong timeout
    await $send->({
        type     => 'websocket.keepalive',
        interval => 30,
        timeout  => 20,
    });

=head4 Disconnect - C<receive> event

Sent when the WebSocket connection is closed, either by the client, server, or due to error conditions.

=over

=item -

C<type> -- C<"websocket.disconnect">

=item -

C<code> (Int) -- WebSocket close code per RFC 6455. When the peer sent a close frame, this is its code (or C<1005>, "no status received", if the frame carried none). When the connection dropped or was aborted with no close handshake -- timeout, write error, server shutdown -- servers MUST report C<1006> ("abnormal closure"). When the server itself closes after a protocol violation, C<code> is the close code it sent (e.g. C<1002> for a framing error or C<1007> for an invalid UTF-8 payload).

=item -

C<reason> (String, default empty) -- For a server-detected abnormal close, the matching standard token from L</Standard Disconnect Reasons>. When the peer sent the Close frame, this is the peer's own reason text, which MAY be empty.

=back

The C<code> carries the RFC 6455 close code and C<reason> supplements it with a
standard token (see L</Standard Disconnect Reasons>), so an application can branch
on one reason vocabulary across HTTP, WebSocket, and SSE. How the two pair up
depends on how the connection ended:

=over 4

=item *

B<The peer sent a Close frame.> C<code> is the peer's own close code (C<1005> if
the frame carried none) and C<reason> is the peer's reason text, commonly empty.
The server does not substitute a PAGI token in this case.

=item *

B<Abnormal drop with no close handshake> -- a bare TCP FIN, a timeout, or a write
failure. C<code> is C<1006> ("abnormal closure") and C<reason> is the token for
the condition: C<client_closed>, C<keepalive_timeout>, C<write_timeout>,
C<write_error>, or C<server_shutdown>.

=item *

B<Server-initiated protocol close> -- the server detected a framing or protocol
violation and sent the Close frame itself. C<code> is the RFC 6455 code for the
fault (C<1002> for a framing/protocol error, C<1007> for an invalid UTF-8
payload) with C<reason> C<protocol_error>; a bounded-queue overflow closes with
C<1008> and C<reason> C<queue_overflow>.

=back

These pairings describe the reference server. The binding contract is only that
C<code> is a valid RFC 6455 close code and C<reason>, when the server can name
one, is a token from L</Standard Disconnect Reasons>; applications SHOULD branch
on C<reason> rather than C<code>.

=head4 Disconnected Client - sending after disconnect

A C<$send> after the connection has closed is a B<no-op>; applications detect a closed WebSocket through the C<websocket.disconnect> event delivered to C<$receive>.

=head4 Close - C<send> event

=over

=item -

C<type> -- C<"websocket.close">

=item -

C<code> (Int, default C<1000>)

=item -

C<reason> (String, default empty)

=back

=head3 WebSocket Protocol Enforcement

A conforming server enforces the framing rules of RFC 6455 and fails the
connection with the appropriate close code when a client violates them. The
reference server sends:

=over

=item -

B<1002 (Protocol Error)> -- a reserved RSV bit is set, a reserved or unknown
opcode is used, a control frame exceeds 125 bytes, or a C<Close> frame carries a
malformed length or an out-of-range close code.

=item -

B<1007 (Invalid Frame Payload Data)> -- a text frame, or a C<Close> frame's
reason, is not valid UTF-8.

=item -

B<1008 (Policy Violation)> -- the application is not draining messages and the
server's inbound queue limit is reached (backpressure shedding).

=back

A frame whose payload exceeds the server's configured C<max_ws_frame_size> fails
the connection; the application observes a C<websocket.disconnect> with code
C<1006> (a server MAY instead send C<1009 Message Too Big>).

B<Transport note:> over HTTP/2 (below) some of these checks are delegated to the
HTTP/2 and frame-parser layers, so the exact code for a given malformation MAY
differ; the UTF-8 (1007) and C<Close>-frame validations apply on both transports.

=head3 WebSocket over HTTP/2 (RFC 8441)

A server MAY accept WebSocket connections over HTTP/2 using the bootstrapping
mechanism of RFC 8441. This is B<transparent to the application>: the C<websocket>
scope and the C<websocket.*> events are identical to HTTP/1.1 and C<http_version>
is C<"2">. The differences are confined to the handshake and live entirely in the
server:

=over

=item -

B<Detection> -- instead of an HTTP/1.1 C<Upgrade>, the client opens an Extended
C<CONNECT> stream (C<:method> = C<CONNECT>, C<:protocol> = C<websocket>), which the
server enables by advertising C<SETTINGS_ENABLE_CONNECT_PROTOCOL>.

=item -

B<Accept> -- C<websocket.accept> is answered with HTTP status B<200>, not the
HTTP/1.1 C<101 Switching Protocols>; C<subprotocol> still maps to
C<Sec-WebSocket-Protocol>, and there is no C<Upgrade> or C<Sec-WebSocket-Accept>
header.

=item -

B<Data and close> -- WebSocket frames travel as HTTP/2 C<DATA> frames on the
stream. Closing the stream (a client C<RST_STREAM>, or END_STREAM) surfaces as a
C<websocket.disconnect>: an abnormal teardown reports code C<1006>, while a clean
WebSocket C<Close> frame carries its own code and reason as on HTTP/1.1.

=back

Servers that do not implement RFC 8441 simply never produce a C<websocket> scope
over HTTP/2; applications need not distinguish the two cases.

=head3 WebSocket Denial Response (extension)

By default an application rejects a handshake by sending C<websocket.close>
before C<websocket.accept>, and the server replies with a bare
C<403 Forbidden>. The B<denial response> extension lets the application reject
with a B<custom> HTTP response instead -- a C<401> with a C<WWW-Authenticate>
header, a C<429> with C<Retry-After>, a redirect, a JSON error body, and so on.
It mirrors ASGI's C<websocket.http.response> extension.

A server that supports it advertises C<<< extensions->{'websocket.http.response'} >>>
as an empty hashref on the C<websocket> scope. To deny, the application sends two
events B<in place of> C<websocket.accept>, only valid before the connection is
accepted:

=over

=item -

B<C<websocket.http.response.start>> -- C<{ status (Int), headers (ArrayRef[ArrayRef[Bytes]]) }>,
structured exactly like C<http.response.start>.

=item -

B<C<websocket.http.response.body>> -- C<{ body (Bytes, default ""), more (Int, default 0) }>,
structured exactly like C<http.response.body> (only the C<body> form applies;
C<file>/C<fh> are not used for denial responses); set C<<< more => 1 >>> to send
the body in multiple events.

=back

The server sends the resulting HTTP response and then closes the connection; the
WebSocket is never established, so no C<websocket.disconnect> is delivered. After
C<websocket.accept> these events are invalid and MUST be ignored.

    # Reject the handshake with a custom 401 instead of a bare 403:
    my $connect = await $receive->();   # websocket.connect
    if (!authorized($scope)) {
        await $send->({
            type    => 'websocket.http.response.start',
            status  => 401,
            headers => [
                [ 'content-type',     'application/json' ],
                [ 'www-authenticate', 'Bearer' ],
            ],
        });
        await $send->({
            type => 'websocket.http.response.body',
            body => '{"error":"unauthorized"}',
        });
        return;
    }
    await $send->({ type => 'websocket.accept' });

Applications SHOULD check for the extension and fall back to C<websocket.close>
when it is absent:

    if ($scope->{extensions}{'websocket.http.response'}) { ... deny with a response ... }
    else                                                 { await $send->({ type => 'websocket.close' }) }

The C<PAGI::WebSocket> helper in C<PAGI-Tools> wraps this as
C<< $ws->deny(status => 401, headers => [...], body => ...) >>, falling back to a
plain close when the server does not advertise the extension.

=head2 Server-Sent Events (SSE)

SSE connections stream C<text/event-stream> data to clients.

=head3 SSE Connection Detection

PAGI servers MUST detect SSE requests and assign a scope of type C<sse> when all of the following are true:

=over

=item -

The C<Accept> header includes the media type C<text/event-stream>.

=item -

The request has not been upgraded to WebSocket.

=back

Otherwise the connection uses a normal C<http> scope.

B<Note on HTTP methods:> SSE works with any HTTP method, not just GET. While the browser's native C<EventSource> API only supports GET, libraries like Microsoft's C<fetch-event-source> (used by htmx 4, datastar, and others) enable SSE over POST, PUT, and other methods via the Fetch API. PAGI servers MUST support SSE for all HTTP methods to enable these modern patterns.

Routing based on URL or application logic is not used to infer SSE.

=head3 SSE Connection Scope

SSE scopes reuse the HTTP scope structure. Servers MUST populate the same keys (C<http_version>, C<method>, C<scheme>, C<path>, C<headers>, C<client>, C<server>, C<state>, the C<pagi> dict, and C<extensions>) but set C<<< type => 'sse' >>>. Header casing rules follow the HTTP section.

=head3 Request Body - C<receive> event

For SSE requests with a body (POST, PUT, etc.), the application receives the body via C<sse.request> events, similar to HTTP:

=over

=item -

C<type> -- C<"sse.request">

=item -

C<body> (Bytes, default C<"">) -- Request body chunk

=item -

C<more> (Int, default C<0>) -- C<1> if more body data is forthcoming, otherwise C<0>

=back

For GET requests (no body), a single C<sse.request> event with empty body and C<<< more => 0 >>> is returned.

B<Example (POST SSE with htmx/datastar):>

    my $event = await $receive->();
    if ($event->{type} eq 'sse.request') {
        my $body = $event->{body};
        # Parse JSON body, extract query parameters, etc.
    }

    await $send->({ type => 'sse.start', status => 200 });
    # ... send SSE events based on POST body ...

=head3 Start SSE - C<send> event

C<sse.start> replaces C<http.response.start> for SSE connections and MUST be sent before any C<sse.send> events.

=over

=item -

C<type> -- C<"sse.start">

=item -

C<status> (Int, default C<200>)

=item -

C<headers> (ArrayRef[ArrayRef[Bytes]]) -- Must include C<<< content-type => 'text/event-stream' >>> unless already supplied by middleware.

=back

On C<sse.start> the server supplies the headers a well-behaved event stream needs
when the application did not: C<Content-Type: text/event-stream> (if absent),
C<Cache-Control: no-cache>, and a C<Date> header. Over HTTP/1.1 it additionally
sends C<Connection: keep-alive> and frames the stream with chunked
C<Transfer-Encoding>; HTTP/2 multiplexes and DATA-frames the stream, so those
connection-specific headers do not apply. Applications need not set any of these.

=head3 Decline SSE - C<send> event

An C<sse> request is an ordinary HTTP C<GET> with C<Accept: text/event-stream>;
the server classifies it as an C<sse> scope before the application runs. Instead
of starting an event stream, an application B<may> decline and return a normal
HTTP response -- a C<404> for an unknown endpoint, a C<401>/C<403> for an
unauthenticated request, a C<204> to tell an C<EventSource> client to stop
reconnecting, a redirect, and so on. It sends two events B<in place of>
C<sse.start>, valid only before the stream has started:

=over

=item -

B<C<sse.http.response.start>> -- C<{ status (Int), headers (ArrayRef[ArrayRef[Bytes]]) }>,
structured exactly like C<http.response.start>.

=item -

B<C<sse.http.response.body>> -- C<{ body (Bytes, default ""), more (Int, default 0) }>,
structured exactly like C<http.response.body>; set C<<< more => 1 >>> to send the
body in multiple events.

=back

These events are namespaced under C<sse.> -- mirroring the
C<websocket.http.response> denial events -- so that an C<sse> scope only ever
carries C<sse.*> events, keeping the server's per-scope dispatch and any
event-type-aware middleware scope-separated.

The server sends the resulting HTTP response and closes the connection; no event
stream is started and no C<sse.disconnect> is delivered. This is a
B<first-send-wins> choice: after C<sse.start>, C<sse.http.response.*> events are
invalid and MUST raise; after C<sse.http.response.start>, the stream events
(C<sse.send> / C<sse.comment> / C<sse.keepalive> / C<sse.close>) are invalid and
MUST raise.

Unlike the WebSocket denial response -- an optional extension with a
C<websocket.close> fallback -- declining an SSE request is a B<core> capability:
there is no event-stream fallback, so every conforming server MUST support it.

    # Decline an unauthenticated SSE request with a 401 instead of streaming:
    if (!authorized($scope)) {
        await $send->({
            type    => 'sse.http.response.start',
            status  => 401,
            headers => [
                [ 'content-type',     'text/plain' ],
                [ 'www-authenticate', 'Bearer' ],
            ],
        });
        await $send->({ type => 'sse.http.response.body', body => 'Unauthorized' });
        return;
    }
    await $send->({ type => 'sse.start', status => 200 });

A C<204 No Content> (or any non-C<200>) response is the supported way to stop an
C<EventSource> client's reconnection loop: the client treats it as a terminal
failure and does not reconnect.

=head3 Send SSE - C<send> event

C<sse.send> emits a single SSE dispatch. Fields marked "String" are Unicode strings per the core data-type rules and MUST be UTF-8 encoded by the server before transmission.

=over

=item -

C<type> -- C<"sse.send">

=item -

C<event> (String, optional)

=item -

C<data> (String) -- Required text payload

=item -

C<id> (String, optional)

=item -

C<retry> (Int, optional) -- Milliseconds for the C<retry:> directive

=back

B<Field validation.> The C<event> and C<id> fields MUST NOT contain newline
(C<\n> or C<\r>) characters: a newline there would forge an SSE frame boundary and
let one dispatch inject another. A server MUST reject such a value -- the reference
server raises, failing the C<send>. This is the SSE-framing case of the same byte
safety the server enforces on response headers (see L</Response Start - C<send>
event>). C<retry> MUST be a non-negative integer.
C<data> is the one field that legitimately contains newlines: the server splits it
on newlines and emits one C<data:> line per segment, preserving multi-line
payloads.

To end the SSE stream the application either returns after its final C<sse.send>,
or sends C<sse.close> explicitly (see L</Close SSE - C<send> event>). On return the
server flushes buffered events and closes the HTTP connection; C<sse.close> does
the same immediately, decoupled from the application returning.

=head3 SSE Comment - C<send> event

C<sse.comment> sends an SSE comment line. Comments start with a colon (C<:>) and are used for keepalive pings or protocol-level messages. Comments do NOT trigger the client's C<onmessage> handler in browsers, making them ideal for connection maintenance.

=over

=item -

C<type> -- C<"sse.comment">

=item -

C<comment> (String) -- Comment text. If the text does not start with C<:>, the server MUST prepend one.

=back

B<Example:>

    # Keepalive ping (no browser callback triggered)
    await $send->({
        type    => 'sse.comment',
        comment => ':keepalive',
    });

The server emits the comment followed by two newlines (C<:keepalive\n\n>). This keeps the connection alive through proxies without triggering application-level event handlers on the client.

=head3 SSE Keepalive - C<send> event

Enables automatic SSE keepalive comments. The server sends comment lines at the specified interval to prevent proxy/load balancer timeouts on idle connections.

=over

=item -

C<type> -- C<"sse.keepalive">

=item -

C<interval> (Number) -- Seconds between keepalive comments. C<0> disables keepalive.

=item -

C<comment> (String, default C<''>) -- Comment text to send. Empty string sends just C<:> followed by newlines.

=back

B<Behavior:>

=over

=item -

Multiple C<sse.keepalive> events update settings (last wins)

=item -

Setting C<<< interval => 0 >>> stops the keepalive timer

=item -

Comments do not trigger client's C<onmessage> handler

=back

B<Example:>

    # Enable keepalive with 30s interval
    await $send->({
        type     => 'sse.keepalive',
        interval => 30,
        comment  => 'ping',
    });

=head3 Close SSE - C<send> event

C<sse.close> ends the SSE stream explicitly. It is the SSE analog of
C<http.response.body> with C<<< more => 0 >>>: the server flushes any buffered
events, writes the end-of-stream marker, and closes the stream. An application
B<may> end a stream either by returning after its final C<sse.send> or by sending
C<sse.close>; the latter is the explicit form and ends the stream B<immediately>,
decoupled from the application returning -- which lets a helper deep in the call
stack end the stream without unwinding to the top.

=over

=item -

C<type> -- C<"sse.close">

=item -

C<reason> (String, optional) -- A server-side reason for logging, metrics, or
tracing (for example C<"job_complete">). The SSE wire protocol has no close frame,
so C<reason> is B<never transmitted to the client>; it is server-side metadata
only, surfaced through the same channels as the C<sse.disconnect> reason. To convey
anything to the client -- including "stop reconnecting" -- send an ordinary
C<sse.send> event before closing and use an HTTP C<204> (or C<retry:>) on the
client's next connection.

=back

After C<sse.close>, any further C<sse.send>, C<sse.comment>, or C<sse.keepalive>
on the same connection B<MUST> raise -- the stream is closed. C<sse.close> is
B<idempotent>: a second C<sse.close> is ignored. C<sse.close> does not by itself
stop the client reconnecting; that is governed by the response to the client's
next connection, not by the close of the current stream.

Unlike C<websocket.close>, C<sse.close> carries B<no> code or client-visible
reason: WebSocket has an RFC 6455 close frame to carry them, while SSE has no such
frame on the wire.

B<Return vs C<sse.close>.> Both terminate the stream and are B<identical on the
wire>: the server writes the end-of-stream marker (the chunked terminator over
HTTP/1.1, C<END_STREAM> over HTTP/2) and closes the stream. A client cannot tell
which path was taken -- C<sse.close> adds nothing to the wire, since its C<reason>
never leaves the server. They differ only in who signals the end, when, and with
what server-side metadata:

=over

=item -

B<Return> sends no end-event: the server infers completion from the application's
returned Future resolving and tears the stream down then. The end is tied to the
application returning.

=item -

B<C<sse.close>> is an explicit C<send> event: the server tears the stream down
B<immediately> on receipt, B<before> the application returns. This lets a helper
deep in the call stack end the stream in place (without unwinding to a return),
frees the client's connection ahead of any slow post-close cleanup the application
still has to perform, and records the C<reason> for the server's access log. After
C<sse.close>, further sends raise (see above) and a subsequent return is a no-op.

=back

When the application's final act is its last C<sse.send> immediately followed by a
return, the two paths are equivalent apart from the server-side C<reason>.

=head3 SSE Disconnect - C<receive> event

Sent to the application if the client disconnects or if the server shuts down the SSE stream after C<sse.start>.

=over

=item -

C<type> -- C<"sse.disconnect">

=item -

C<reason> (String) -- Standard reason string from L</Standard Disconnect Reasons>

=back

B<Common reasons:>

=over 4

=item C<client_closed>

Client closed the connection

=item C<write_error>

Failed to write to the socket (keepalive or event)

=item C<write_timeout>

Send timeout exceeded

=item C<server_shutdown>

Server shut down the stream

=back

=head2 Transport Flow Control

The C<pagi.transport> scope key provides a synchronous, read-only handle for
inspecting B<outbound flow control> -- how many bytes the server has queued for
the client but not yet written to the network. It lets an application observe
backpressure and make its own delivery decisions (conflate, coalesce, shed
load, or disconnect a slow client) instead of only blocking until the buffer
drains. It is the server-side analogue of the browser WebSocket API's
C<bufferedAmount>, generalized across the C<http>, C<websocket>, and C<sse>
scope types.

Servers SHOULD provide C<pagi.transport> for C<http>, C<websocket>, and C<sse>
scopes. A server that cannot determine its outbound buffer state omits the key;
applications MUST treat its absence as "flow-control introspection unavailable"
(and the high-level helpers report a buffered amount of C<0>).

The presence of C<pagi.transport> affects introspection, not the fundamental
send contract. Applications self-pace by awaiting every C<$send> Future whether
or not this object is present; the object additionally lets them observe queue
depth and make an earlier policy decision to conflate, coalesce, shed load, or
disconnect.

The long-term goal is universal coverage: every scope type that produces a
stream of outbound events provides C<pagi.transport>, across every transport a
server supports, so an application need not know which transport or protocol
version carries its events. A missing key therefore signals a B<capability
limitation> -- a server still being built out, or a transport whose outbound
buffer the server cannot yet measure -- not a steady state for applications to
design around. Write transport-agnostic code: treat the key's presence as the
norm and tolerate its absence per the contract above.

=head3 Methods

B<C<buffered_amount()>> -- Returns the number of bytes queued for the client but
not yet written to the network, as an integer; C<0> when the send buffer is
fully drained. This is a synchronous, non-blocking, non-destructive read -- it
neither sends, receives, nor consumes queued messages.

    my $pending = $scope->{'pagi.transport'}->buffered_amount;

If a server provides C<pagi.transport>, it B<must> implement C<buffered_amount>.

B<C<high_water_mark()>> -- Returns the buffered-byte threshold at or above which
the server applies backpressure: the Future returned by a C<$send> that would
exceed it remains pending until the buffer drains enough for the server to
continue processing the event. Awaiting that Future suspends the application
coroutine, not the Perl thread. Applications use the threshold relative to the
ceiling rather than hard-coding a byte count. Returns C<undef> if the server has
no fixed high-water mark.

B<C<low_water_mark()>> -- Returns the buffered-byte threshold the buffer must
fall back to before the server releases backpressure (the drain point). Returns
C<undef> if not applicable.

Servers SHOULD implement C<high_water_mark> and C<low_water_mark> where they use
a watermark-based backpressure model.

=head3 Backpressure callbacks

The synchronous reads above suit a producer that checks before each send. A
producer that B<cannot> self-pace with a blocking C<$send> -- an event-driven
source that pushes a message per incoming event, say -- instead wants to be
B<told> when to pause and resume. For that, the handle provides two
edge-triggered callbacks.

B<C<on_high_water($callback)>> -- Registers a callback invoked when the outbound
buffer reaches or exceeds C<high_water_mark> (backpressure engaged). If the
buffer is already at or above the mark when the callback is registered, the
callback is invoked immediately.

    $transport->on_high_water(sub { $source->pause });

B<C<on_drain($callback)>> -- Registers a callback invoked when the outbound
buffer falls back below C<low_water_mark> after having reached the high mark
(backpressure released). It is not invoked merely because the buffer is below
the low mark at registration time -- only on an actual high-then-low transition.

    $transport->on_drain(sub { $source->resume });

The two form a hysteresis cycle: C<on_high_water> fires once when the buffer
crosses up to the high mark, then C<on_drain> fires once when it falls back
below the low mark, then the cycle re-arms. The gap between the marks prevents
flapping when the buffered amount hovers near a single threshold. Multiple
callbacks may be registered for each and are invoked in registration order; the
callbacks receive no arguments (read C<buffered_amount> if the current depth is
needed).

Servers that provide C<pagi.transport> with a watermark-based backpressure model
SHOULD implement C<on_high_water> and C<on_drain>.

=head3 Applicability

=over 4

=item C<http>

Bytes queued for the current (streaming) response.

=item C<websocket>

Bytes queued for the WebSocket session.

=item C<sse>

Bytes queued for the SSE stream.

=back

=head3 Example: conflating a live feed under backpressure

When a client falls behind on a high-frequency stream, sending the next update
only while the backlog is small keeps the client B<current> (sparse but fresh)
rather than B<stale> (dense but lagging):

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

    while (1) {
        my $update = await next_update();

        # Skip this frame if the client is already behind; it will get the
        # next, fresher one. Threshold relative to the server's ceiling.
        if ($transport) {
            my $ceiling = $transport->high_water_mark // 65536;
            next if $transport->buffered_amount > $ceiling / 2;
        }

        await $send->({ type => 'websocket.send', text => $update });
    }

The high-level helpers in L<PAGI::WebSocket>, L<PAGI::SSE>, and L<PAGI::Request>
expose these as C<< $obj->buffered_amount >>, C<< $obj->high_water_mark >>, and
C<< $obj->low_water_mark >>.

=head2 PAGI to PSGI Compatibility

PAGI translates keys explicitly to maintain compatibility with PSGI:

=over

=item -

C<REQUEST_METHOD> -> C<method>

=item -

C<SCRIPT_NAME> -> C<root_path>

=item -

C<PATH_INFO> -> C<path> minus C<root_path>

=item -

C<QUERY_STRING> -> C<query_string>

=item -

C<CONTENT_TYPE> -> extracted from C<headers>

=item -

C<CONTENT_LENGTH> -> extracted from C<headers>

=item -

C<SERVER_NAME>, C<SERVER_PORT> -> C<server>

=item -

C<REMOTE_ADDR>, C<REMOTE_PORT> -> C<client>

=item -

C<SERVER_PROTOCOL> -> C<http_version>

=item -

C<psgi.url_scheme> -> C<scheme>

=item -

C<psgi.version> -> C<[1, 1]> (PAGI servers MUST advertise the PSGI version they emulate when bridging)

=item -

C<psgi.input> -> constructed from C<http.request> events

=item -

C<psgi.errors> -> handled by the server as appropriate

=item -

C<psgi.streaming>, C<psgi.nonblocking>, C<psgi.multithread>, C<psgi.multiprocess> -> derived from PAGI server capabilities and advertised via PSGI adapter docs

=back

Response mappings:

=over

=item -

C<status> and C<headers> map directly to C<http.response.start>

=item -

Body content from PSGI maps directly to C<http.response.body> messages.

=back

=head2 PAGI Encoding Differences

=over

=item -

C<path>: Decoded UTF-8 string from percent-encoded input. The server first
percent-decodes C<raw_path>, then attempts UTF-8 decoding of the resulting
bytes into Unicode characters. If the bytes are not valid UTF-8, the server
B<should> fall back to the original percent-decoded bytes rather than
replacing invalid sequences or rejecting the request (Mojolicious-style
fallback). Applications needing strict UTF-8 validation can check C<raw_path>
and decode themselves with C<Encode::FB_CROAK>.

=item -

C<headers>: Represented as bytes exactly as sent/received

=item -

C<query_string>: Raw bytes from URL after C<?>, percent-encoded

=item -

C<root_path>: Unicode path string matching C<SCRIPT_NAME>

=back

=head2 Version History

=over

=item -

C<0.3> (Draft): C<pagi.transport> flow control; C<websocket.http.response>
denial-response extension; C<on_complete> plus reworked abnormal-only disconnect
reasons; C<response_started> (MUST) and C<response_complete> (SHOULD) connection-state
accessors for per-request response progress; documented server-supplied behaviors and
WebSocket over HTTP/2; removed C<pagi.features> and the per-send C<timeout> field.

=item -

C<0.2> (Draft): SSE POST method support (C<sse.request>), keepalive events,
disconnect reasons, C<pagi.connection>, clarified scope fields.

=item -

C<0.1> (Draft): Initial draft based on ASGI 2.5, supporting HTTP, WebSocket, and SSE.

=back

=head2 Copyright

This document has been placed in the public domain.
