=encoding UTF-8

=head1 NAME

PAGI::Tutorial - Learn the PAGI protocol for async Perl web applications

=head1 DESCRIPTION

PAGI (Perl Asynchronous Gateway Interface) is a B<protocol>, not a framework.
It is a small, well-defined contract for hooking an asynchronous Perl
application up to an asynchronous server: your application is an C<async sub>
that receives a connection scope and two coderefs (one to receive events from
the client, one to send events back), and that is essentially the whole
specification. PAGI also defines a loop-agnostic way to model custom
asynchronous events (HTTP, WebSocket, Server-Sent Events, lifespan, and your
own), so the same application code runs unchanged on any conforming server,
under any event loop.

This tutorial teaches the protocol itself. To run the examples you need a
server, and we use L<PAGI::Server> (the reference implementation, from the
C<PAGI-Server> distribution) because it is a complete, fully-featured PAGI
server. Nothing in the protocol depends on it; any conforming server would do.

The only hard requirement for a PAGI application is L<Future::AsyncAwait> for
async/await syntax. The event loop (L<IO::Async>, in PAGI::Server's case) is an
implementation detail of the server, not of your application.

Along the way we occasionally point at the C<PAGI-Tools> distribution, which
provides convenience helpers (middleware, routers, request/response sugar,
ready-made apps). Those are B<entirely optional>: ordinary PAGI applications
and helpers built I<on top of> the protocol, never required to write a complete
PAGI application. For a guide to them, see L<PAGI::Tools::Tutorial>.

=head1 PART 1: GETTING STARTED

=head2 1.1 Introduction

=head3 What is PAGI?

PAGI (Perl Asynchronous Gateway Interface) is a B<protocol> for building
asynchronous web applications in Perl: a specification for how an async
application and an async server talk to each other. It is a spiritual successor
to PSGI that embraces modern async/await patterns using L<Future::AsyncAwait>
and provides first-class support for WebSockets, Server-Sent Events, and
HTTP/1.1.

Unlike traditional synchronous web frameworks, PAGI allows your application to handle multiple concurrent connections efficiently without blocking, making it ideal for:

=over 4

=item * WebSocket applications (real-time chat, live updates)

=item * Server-Sent Events (SSE) for streaming data

=item * Long-polling and streaming responses

=item * High-concurrency applications

=item * Applications that make multiple I/O calls per request

=back

=head3 Why Async?

Traditional synchronous web frameworks handle one request at a time per process. When your application waits for a database query, an external API call, or file I/O, the entire process is blocked and can't handle other requests.

B<The Traditional Solution: Forking Servers>

Perl web applications have traditionally used pre-forking servers like L<Starman> to achieve concurrency. Starman spawns multiple worker processes, each handling one request at a time. This approach works well and has served the Perl community for years:

  Approach        | Forking (Starman)           | Async (PAGI)
  ----------------|-----------------------------|--------------------------
  Concurrency     | Multiple processes          | Single process, event loop
  Memory          | Each worker copies app      | Shared memory, lower usage
  Connections     | Limited by worker count     | Thousands per process
  WebSocket/SSE   | Impractical (ties up worker)| Natural fit
  Blocking I/O    | Fine (isolated per worker)  | Must use async libraries
  CPU-bound work  | Fine (isolated per worker)  | Blocks event loop (use workers)
  Debugging       | Simpler (sequential code)   | Trickier (async flow)
  Existing code   | Works as-is                 | May need async adapters

B<When to use which:>

=over 4

=item * B<Starman/forking> - CPU-heavy work, existing sync codebases, simple request/response apps, blocking database drivers

=item * B<PAGI/async> - WebSockets, SSE, long-polling, high connection counts, I/O-bound apps, real-time features

=back

Many applications benefit from both the async model (for real-time endpoints like WebSocket and SSE) and pre-forking (for CPU-bound request/response work). The reference server, L<PAGI::Server>, combines them with its worker mode (C<--workers N>): several pre-forked workers, each running the async event loop. That is a feature of the server, not of the PAGI protocol.

B<The Async Advantage>

Asynchronous programming allows your application to handle multiple requests concurrently in a single process. When one request is waiting for I/O, the event loop can switch to another request, dramatically improving resource utilization and response times.

This is especially important for:

=over 4

=item * Real-time bidirectional communication (WebSockets)

=item * Keeping connections open for streaming (SSE)

=item * Applications that make multiple external API calls

=item * High-traffic applications where blocking I/O is a bottleneck

=back

=head3 PAGI vs PSGI

If you're familiar with PSGI, here are the key differences:

  Feature          | PSGI                    | PAGI
  -----------------|-------------------------|---------------------------
  Execution Model  | Synchronous             | Asynchronous (async/await)
  Application Sig  | sub { $env }            | async sub { $scope, $receive, $send }
  Request Metadata | %env hash               | %scope hash
  Request Body     | $env->{'psgi.input'}    | await $receive->()
  Response         | [\@status, \@headers, \@body] | await $send->({...})
  WebSocket        | Not supported           | First-class support
  SSE              | Hacky streaming         | First-class support
  Backpressure     | No explicit control     | Explicit via Futures

The async model means you can write code that looks synchronous (thanks to async/await) while getting all the benefits of non-blocking I/O.

=head3 Prerequisites

To use PAGI, you'll need:

=over 4

=item * B<Perl 5.18 or later> - the floor for the PAGI distributions (the specification, the reference server, and the toolkit)

=item * L<Future::AsyncAwait> - Provides async/await syntax

=item * Basic understanding of Futures and async programming (we'll cover the essentials)

=back

B<Note:> A bare PAGI application's only hard dependency is C<Future::AsyncAwait>
(for async/await syntax), which supports Perl back to 5.16; the event loop
(L<IO::Async>) is an implementation detail of the server, and using C<Future::IO>
for timers and I/O keeps your code portable across servers. In practice, though,
you run on something: the specification, the reference server L<PAGI::Server>,
and the L<PAGI-Tools|PAGI::Tools> toolkit all require B<Perl 5.18 or later>, and
another server or a higher-level framework may require a newer Perl still. The
minimum version you need is the floor of whatever you build on.

=head2 1.2 Installation

Installing PAGI from CPAN:

  cpanm PAGI

Or from source:

  git clone https://github.com/jjn1056/pagi.git
  cd PAGI
  cpanm --installdeps .

To include development dependencies (for testing):

  cpanm --installdeps . --with-develop

Verify the installation:

  pagi-server --version

Installing C<PAGI> currently also pulls in the C<PAGI-Server> reference server
and the C<PAGI-Tools> helpers for backward compatibility during the transition;
see L<PAGI/INSTALLATION AND BACKWARD COMPATIBILITY>.

=head3 About pagi-server

Throughout this tutorial, we'll use C<pagi-server> to run our examples. L<PAGI::Server>
is the reference implementation of a PAGI server - it's a fully-featured async HTTP
server built on L<IO::Async> that supports HTTP/1.1, WebSockets, Server-Sent Events,
TLS, and multi-worker mode.

At the time of writing, C<pagi-server> is the only PAGI-compatible server available.
However, the PAGI specification is designed to be server-agnostic, so future servers
(perhaps built on different event loops like L<Mojo::IOLoop> or L<AnyEvent>) could
run the same application code without modification.

=head2 1.3 Your First PAGI App

Let's create a simple "Hello, World!" application. Create a file called C<hello.pl>:

  use strict;
  use warnings;
  use Future::AsyncAwait;

  async sub app {
      my ($scope, $receive, $send) = @_;

      # Only handle HTTP requests (server also sends lifespan events)
      die "Expected http scope" unless $scope->{type} eq 'http';

      # Send the response status and headers
      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain']],
      });

      # Send the response body
      await $send->({
          type => 'http.response.body',
          body => 'Hello, World!',
      });
  }

  # Return a reference to the app subroutine
  \&app;

Let's break down what's happening:

=over 4

=item 1. C<async sub app> - Declares an asynchronous subroutine using L<Future::AsyncAwait>

=item 2. C<($scope, $receive, $send)> - The three parameters every PAGI app receives:

=over 4

=item * C<$scope> - Hash reference containing request metadata (method, path, headers, etc.)

=item * C<$receive> - Async code reference for receiving events from the client

=item * C<$send> - Async code reference for sending events to the client

=back

=item 3. C<die "Expected http scope" unless $scope-E<gt>{type} eq 'http'> - The server calls your app for different event types (HTTP requests, WebSocket, lifespan). Applications must throw an exception for unrecognized scope types to prevent silent protocol mismatches.

=item 4. C<await $send-E<gt>({...})> - Sends events asynchronously, waiting for each to complete

=item 5. C<\&app> - Returns a reference to the application subroutine

=back

Run your application:

  pagi-server hello.pl --port 5000

Test it:

  curl http://localhost:5000/

You should see:

  Hello, World!

Try accessing it from a browser by visiting C<http://localhost:5000/>.

=head3 Environment Modes

pagi-server supports environment modes that control middleware behavior:

=over 4

=item B<development> (default when running interactively)

Auto-enables L<PAGI::Middleware::Lint> with strict mode, which catches PAGI
specification violations early and provides helpful error messages. Note that
Lint lives in the C<PAGI-Tools> distribution: pagi-server applies it only when
PAGI-Tools is installed, and otherwise prints a reminder that you can install
PAGI-Tools to enable it and runs without it.

=item B<production> (default when running non-interactively)

No middleware is auto-enabled, and access logging is disabled for maximum
performance. This is the default when running under systemd, docker, cron,
or any non-TTY environment. Use C<--access-log FILE> or C<--access-log FILE>
to enable logging.

=item B<none>

Explicit opt-out of all auto-middleware, regardless of TTY detection.

=back

Mode is determined by (in order of precedence):

  1. -E / --env command line flag
  2. PAGI_ENV environment variable
  3. Auto-detection: TTY = development, no TTY = production

After determining the mode, pagi-server sets C<PAGI_ENV> to the resolved
value. Your app can check C<$ENV{PAGI_ENV}> to know what mode it's running
in, similar to Plack's C<PLACK_ENV>.

Examples:

  # Auto-detect mode (TTY = development)
  pagi-server hello.pl

  # Explicit production mode
  pagi-server -E production hello.pl

  # Via environment variable
  PAGI_ENV=production pagi-server hello.pl

  # Disable auto-middleware even in development
  pagi-server --no-default-middleware hello.pl

The development mode Lint middleware helps catch common mistakes like forgetting
to C<await> your C<$send> calls or returning without sending a response.

=head3 Inspecting the Scope

The C<$scope> hash contains metadata about the current connection - similar to PSGI's C<$env> hash. It tells you everything about the request before you read the body: the HTTP method, path, headers, query string, client address, and more. Unlike PSGI where everything is in one flat hash, PAGI separates the metadata (C<$scope>) from the body (read via C<$receive>).

Let's modify the app to show what's in C<$scope>:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use Data::Dumper;

  async sub app {
      my ($scope, $receive, $send) = @_;

      die "Expected http scope" unless $scope->{type} eq 'http';

      my $body = Dumper($scope);

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain']],
      });

      await $send->({
          type => 'http.response.body',
          body => $body,
      });
  }

  \&app;

Run this and visit C<http://localhost:5000/test?foo=bar>. You'll see the scope hash which includes:

=over 4

=item * C<type> - Connection type (http, websocket, sse, lifespan)

=item * C<method> - HTTP method (GET, POST, etc.)

=item * C<path> - URL path (decoded UTF-8)

=item * C<raw_path> - URL path (raw bytes)

=item * C<query_string> - Query string (raw bytes)

=item * C<headers> - Array reference of [name, value] pairs

=item * C<server> - Server address and port

=item * And more...

=back

=head2 1.4 Understanding the Event Loop: Blocking vs Non-Blocking

Understanding the event loop is crucial to writing efficient PAGI applications. Let's visualize how it works.

=head3 The Event Loop

PAGI servers use an event loop to manage asynchronous operations. Here's a simplified view of how the event loop handles multiple connections:

  Single-Threaded Event Loop

  Time -->

  Request A: [======API Call (async)======]----[Process]--[Send]
  Request B:     [--DB Query (async)--]--------[Process]--------[Send]
  Request C:         [Timer (async)]----[Process]----[Send]

  Event Loop: [A][B][C][A][B][C][B][A][C][B][A][C][A][B]
              |  |  |  |  |  |  |  |  |  |  |  |  |  |
              Continuously switching between requests as they wait

When you use C<await>, you're telling the event loop "I'm waiting for something, go handle other requests until this completes." The event loop can then switch to another request, making efficient use of CPU time.

=head3 Non-Blocking Operations (GOOD)

These operations yield control to the event loop:

  use Future::AsyncAwait;
  use Future::IO;

  async sub good_app {
      my ($scope, $receive, $send) = @_;

      # Good: Async sleep (doesn't block) - loop-agnostic!
      await Future::IO->sleep(5);

      # Good: Async HTTP client (doesn't block)
      # Use Net::Async::HTTP, HTTP::Tiny::Async, or similar
      # my $response = await $http->GET('https://api.example.com/data');

      # Good: Async database query (using an async DB driver)
      # my $row = await $db->selectrow_async('SELECT * FROM users WHERE id = ?', $user_id);

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain']],
      });

      await $send->({
          type => 'http.response.body',
          body => 'Non-blocking operations completed!',
      });
  }

  \&good_app;

=head3 Blocking Operations (BAD)

These operations block the entire process:

  use Future::AsyncAwait;
  use LWP::UserAgent;  # Synchronous HTTP client
  use DBI;             # Most DBI drivers are synchronous

  async sub bad_app {
      my ($scope, $receive, $send) = @_;

      # BAD: Blocks for 5 seconds, prevents other requests from being handled
      sleep 5;

      # BAD: Synchronous HTTP request blocks until complete
      my $ua = LWP::UserAgent->new;
      my $response = $ua->get('https://api.example.com/data');

      # BAD: Synchronous database query blocks
      # my $dbh = DBI->connect(...);
      # my $row = $dbh->selectrow_hashref('SELECT * FROM users WHERE id = ?', undef, $user_id);

      # BAD: Expensive CPU operation blocks (e.g., bcrypt password hashing)
      # my $hashed = bcrypt_hash($password);  # Takes ~100ms

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain']],
      });

      await $send->({
          type => 'http.response.body',
          body => 'Blocking operations completed (badly)!',
      });
  }

  \&bad_app;

=head3 Why Blocking is Bad

Let's do the math:

=over 4

=item * You have 1,000 concurrent connections

=item * Each request needs to make a synchronous database query that takes 100ms

=item * With a blocking operation, each connection is stuck for 100ms

=item * 1,000 connections x 100ms = 100 seconds of total blocked time

=item * With async operations, all 1,000 queries can be in-flight simultaneously

=back

In a single-threaded event loop, one blocking operation prevents ALL other connections from making progress. A single C<sleep(5)> will freeze your entire application for 5 seconds!

=head3 Solutions for Blocking Operations

When you must use blocking operations, you have options:

=over 4

=item 1. B<Use Async Libraries>

Prefer async alternatives when available:

=over 4

=item * C<Future::IO-E<gt>sleep($secs)> instead of C<sleep()> - loop-agnostic!

=item * L<Net::Async::HTTP> instead of L<LWP::UserAgent>

=item * L<Net::Async::Redis> instead of synchronous Redis clients

=item * L<Database::Async> for async database access (PostgreSQL, MySQL, SQLite)

=back

=item 2. B<Use a Worker Pool for CPU-bound Work>

For CPU-intensive operations (password hashing, image processing), offload to
a subprocess. See L<PAGI::Cookbook> for patterns using L<IO::Async::Function>
or external job queues.

=back

B<What about worker mode?> Running with C<--workers N> does NOT solve blocking for individual connections. If a worker has a WebSocket connection and does a blocking operation, that WebSocket will freeze until the operation completes. Worker mode only means other connections on other workers aren't affected. See section 1.5 for when worker mode is actually useful.

=head2 1.5 Worker Mode

L<PAGI::Server> supports two deployment modes: single-process and multi-worker.

=head3 Single Process vs Worker Mode

  Single Process Mode:

  [Master Process]
    |
    +-- Runs event loop
    +-- Handles all connections
    +-- Good for: async apps, development, low-medium traffic


  Worker Mode:

  [Master Process]
    |
    +-- [Worker 1] (handles connections)
    +-- [Worker 2] (handles connections)
    +-- [Worker 3] (handles connections)
    +-- [Worker 4] (handles connections)

  Good for: multi-core utilization, crash isolation, high traffic

=head3 When to Use Worker Mode

  Scenario                           | Single Process | Worker Mode
  -----------------------------------|----------------|-------------
  Pure async app (no blocking)       | Yes            | Yes
  Multi-core server in production    | Wastes cores   | Yes
  Crash isolation needed             | No             | Yes
  WebSocket/SSE with shared state    | Yes            | Needs PubSub
  Development/testing                | Yes            | Optional

B<Important:> Worker mode does NOT fix blocking operations. Each worker still runs a single-threaded event loop. If you do a blocking operation in a worker, ALL connections on that worker freeze (including any WebSocket or SSE connections). To avoid blocking, use async libraries as described in section 1.4.

=head3 Running with Workers

Start the server with multiple worker processes:

  pagi-server hello.pl --port 5000 --workers 4

This creates:

=over 4

=item * 1 master process that accepts connections

=item * 4 worker processes that handle requests

=item * Automatic worker respawning if a worker crashes

=back

The number of workers should typically match your CPU core count:

  # Check your CPU core count and set workers accordingly
  # On Linux: nproc   On macOS: sysctl -n hw.ncpu
  pagi-server hello.pl --port 5000 --workers 4

=head3 Worker Isolation

B<Important:> Each worker process is isolated. They do NOT share:

=over 4

=item * Memory (variables are per-worker)

=item * Database connections

=item * WebSocket connections

=item * Application state

=back

=head3 Wrong: Shared State in Memory

This will NOT work correctly across workers:

  # DON'T DO THIS in worker mode!
  my $counter = 0;  # Each worker has its own $counter

  async sub app {
      my ($scope, $receive, $send) = @_;

      $counter++;  # Only increments in THIS worker

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain']],
      });

      await $send->({
          type => 'http.response.body',
          body => "Counter: $counter",  # Will show different values per worker
      });
  }

  \&app;

=head3 Right: External State Storage

Use a shared backend for state. For example with Redis (see L<PAGI::Cookbook>
for complete Redis examples):

  use Future::AsyncAwait;

  # Assume $redis is set up during lifespan startup
  # See PAGI::Cookbook for Redis connection patterns

  async sub app {
      my ($scope, $receive, $send) = @_;

      # Increment counter in Redis (shared across all workers)
      my $counter = await $redis->incr('request_counter');

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain']],
      });

      await $send->({
          type => 'http.response.body',
          body => "Counter: $counter",  # Accurate across all workers
      });
  }

  \&app;

For WebSocket applications that need to broadcast messages across workers, use an external message broker like Redis pub/sub.

=head1 PART 2: UNDERSTANDING RAW PAGI

In Part 1, we covered the basics of PAGI applications and the event loop. Now we'll dive deeper into the raw PAGI protocol, exploring how to work directly with the three arguments (C<$scope>, C<$receive>, and C<$send>) before introducing higher-level abstractions.

Understanding raw PAGI is important because:

=over 4

=item * It helps you understand what's happening under the hood

=item * You can build custom middleware and utilities

=item * You'll appreciate what frameworks like L<PAGI::Endpoint::Router> do for you

=item * You can handle edge cases that frameworks might not cover

=back

=head2 2.1 The Three Arguments

Every PAGI application receives exactly three arguments:

  async sub app {
      my ($scope, $receive, $send) = @_;
      # ... your code here
  }

Let's examine each in detail.

=head3 $scope - Connection Metadata

C<$scope> is a hash reference containing metadata about the connection and request. The contents vary depending on the connection type.

For HTTP requests (C<type =E<gt> 'http'>):

  {
      type => 'http',
      method => 'GET',               # HTTP method
      path => '/users/123',          # URL path (decoded UTF-8)
      raw_path => '/users/123',      # URL path (raw bytes)
      query_string => 'foo=bar',     # Query string (raw bytes)
      headers => [                   # Array of [name, value] pairs
          ['host', 'localhost:5000'],
          ['user-agent', 'curl/7.68.0'],
          ['content-type', 'application/json'],
      ],
      server => ['127.0.0.1', 5000], # Server address and port
      client => ['127.0.0.1', 54321],# Client address and port
      scheme => 'http',              # 'http' or 'https'
      http_version => '1.1',         # HTTP version
      extensions => { ... },         # PAGI extensions
  }

For WebSocket connections (C<type =E<gt> 'websocket'>):

  {
      type => 'websocket',
      path => '/ws',
      headers => [ ... ],
      subprotocols => ['chat', 'superchat'],  # Requested subprotocols
      # ... other fields similar to http
  }

For SSE connections (C<type =E<gt> 'sse'>):

  {
      type => 'sse',
      path => '/events',
      headers => [ ... ],
      # ... other fields similar to http
  }

For lifespan events (C<type =E<gt> 'lifespan'>):

  {
      type => 'lifespan',
  }

=head3 $receive - Receiving Client Events

C<$receive> is an asynchronous code reference that returns a L<Future>. Call it to receive the next event from the client:

  my $event = await $receive->();

The structure of C<$event> depends on the connection type and what's happening.

For HTTP requests, you receive body chunks:

  {
      type => 'http.request',
      body => '{"name":"John"}',  # Raw bytes
      more => 0,                  # 0 = last chunk (default), 1 = more coming
  }

For WebSocket, you receive messages and disconnect events:

  # Message received
  {
      type => 'websocket.receive',
      text => 'Hello!',           # For text messages
      # OR
      bytes => "\x00\x01\x02",    # For binary messages
  }

B<Why separate C<text> and C<bytes> keys?> The WebSocket protocol itself
distinguishes text frames (must be valid UTF-8) from binary frames (arbitrary
bytes) at the wire level. Having separate keys provides three benefits: (1) you
can use both on the same connection - e.g., text for JSON commands, binary for
audio streams; (2) type safety - you know exactly what you received without
ambiguity; (3) you can't accidentally mishandle encoding. If you don't care
about the distinction: C<my $data = $event-E<gt>{text} // $event-E<gt>{bytes}>.
The L<PAGI::WebSocket> helper class abstracts this for most use cases.

  # Connection closed
  {
      type => 'websocket.disconnect',
      code => 1000,               # Close code
      reason => 'Normal closure', # Close reason
  }

For lifespan, you receive startup and shutdown events:

  { type => 'lifespan.startup' }
  { type => 'lifespan.shutdown' }

=head3 $send - Sending Events to Client

C<$send> is an asynchronous code reference that sends events to the client. It returns a L<Future> that completes when the event has been sent:

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

The events you send depend on the connection type.

=head2 2.2 Scope Types

PAGI defines four scope types, each representing a different kind of connection or event.

=head3 http - Standard HTTP Requests

The most common type. Used for normal HTTP request/response cycles.

Check for it:

  if ($scope->{type} eq 'http') {
      # Handle HTTP request
  }

=head3 websocket - WebSocket Connections

Used for bidirectional real-time communication. WebSocket connections start as HTTP upgrade requests.

Check for it:

  if ($scope->{type} eq 'websocket') {
      # Handle WebSocket connection
  }

See L<PAGI::WebSocket> for WebSocket utilities.

=head3 sse - Server-Sent Events

A PAGI extension for server-to-client streaming. SSE allows the server to push updates to the client over a long-lived HTTP connection.

Check for it:

  if ($scope->{type} eq 'sse') {
      # Handle SSE connection
  }

See L<PAGI::SSE> for SSE utilities.

=head3 lifespan - Application Lifecycle Events

Used for application startup and shutdown events. This allows your application to initialize resources (database connections, caches, etc.) when it starts and clean them up when it shuts down.

Check for it:

  if ($scope->{type} eq 'lifespan') {
      # Handle startup/shutdown
  }

Note: L<PAGI::Endpoint::Router> handles this via C<on_startup> and C<on_shutdown> callbacks.

=head2 2.3 HTTP Request/Response Cycle (Raw)

Let's build a complete HTTP request handler that reads the request body and echoes it back.

  use strict;
  use warnings;
  use Future::AsyncAwait;

  async sub app {
      my ($scope, $receive, $send) = @_;

      # Only handle HTTP requests
      die "Expected http scope" unless $scope->{type} eq 'http';

      # Read the entire request body
      # NOTE: This loop does NOT block the event loop! The 'await' keyword
      # yields control while waiting for data, allowing other requests to
      # be processed. The loop only runs when there's actually data.
      my $body = '';
      while (1) {
          my $event = await $receive->();
          $body .= $event->{body} if defined $event->{body};
          last unless $event->{more};  # Stop when no more chunks
      }

      # Prepare response body
      my $response_body = "You sent: " . (length($body) ? $body : "(empty)");

      # Send response start (status and headers)
      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [
              ['content-type', 'text/plain'],
              ['content-length', length($response_body)],
          ],
      });

      # Send response body (more => 0 is the default, so can be omitted)
      await $send->({
          type => 'http.response.body',
          body => $response_body,
      });
  }

  \&app;

Test it:

  # Terminal 1: Start server
  pagi-server echo.pl --port 5000

  # Terminal 2: Send a request with body
  curl -X POST -d "Hello, PAGI!" http://localhost:5000/

  # Output: You sent: Hello, PAGI!

Key points:

=over 4

=item * Always check C<more> to know when the request body is complete

=item * Send C<http.response.start> before sending any body

=item * C<more =E<gt> 0> is the default for final chunks (can be omitted)

=item * The request body is raw bytes (you may need to decode it)

=back

=head2 2.4 WebSocket (Raw)

WebSocket enables bidirectional real-time communication. Here's a simple echo server:

  use strict;
  use warnings;
  use Future::AsyncAwait;

  async sub app {
      my ($scope, $receive, $send) = @_;

      # Only handle WebSocket connections
      die "Expected websocket scope" unless $scope->{type} eq 'websocket';

      # Accept the WebSocket connection
      await $send->({
          type => 'websocket.accept',
          # Optional: specify a subprotocol from $scope->{subprotocols}
          # subprotocol => 'chat',
      });

      # Echo loop: receive messages and send them back
      # NOTE: This loop does NOT block! The 'await' yields to the event
      # loop while waiting, allowing other connections to be processed.
      while (1) {
          my $event = await $receive->();

          # Handle disconnection
          if ($event->{type} eq 'websocket.disconnect') {
              warn "Client disconnected: code=$event->{code}, reason=$event->{reason}\n";
              last;
          }

          # Echo text messages
          if (defined $event->{text}) {
              await $send->({
                  type => 'websocket.send',
                  text => "Echo: $event->{text}",
              });
          }
          # Echo binary messages
          elsif (defined $event->{bytes}) {
              await $send->({
                  type => 'websocket.send',
                  bytes => $event->{bytes},
              });
          }
      }
  }

  \&app;

Test it with a WebSocket client:

  # JavaScript in browser console:
  const ws = new WebSocket('ws://localhost:5000/');
  ws.onmessage = (event) => console.log('Received:', event.data);
  ws.onopen = () => ws.send('Hello, WebSocket!');

  // You should see: Received: Echo: Hello, WebSocket!

Key points:

=over 4

=item * Must send C<websocket.accept> before sending any messages

=item * Check C<type eq 'websocket.disconnect'> to detect when the client closes the connection

=item * Messages can be text (C<text>) or binary (C<bytes>)

=item * The receive loop blocks waiting for the next message

=back

See L<PAGI::WebSocket> for higher-level utilities that make WebSocket handling easier.

To reject a handshake, send C<websocket.close> before C<websocket.accept> (a bare
C<403>), or -- where the server advertises the C<websocket.http.response>
extension -- a C<websocket.http.response.start>/C<.body> pair for a custom
response such as a C<401>. See the cookbook's "Rejecting a handshake with a
custom response" and L<PAGI::Spec::Www/"WebSocket Denial Response (extension)">.

=head2 2.5 SSE (Raw)

Server-Sent Events allow the server to push updates to clients over HTTP. Here's a clock that sends the current time every second:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use Future::IO;

  async sub app {
      my ($scope, $receive, $send) = @_;

      # Only handle SSE connections
      die "Expected sse scope" unless $scope->{type} eq 'sse';

      # Start the SSE response
      await $send->({
          type => 'sse.response.start',
          status => 200,
          headers => [
              ['cache-control', 'no-cache'],
              ['x-custom-header', 'example'],
          ],
      });

      # Send the current time every second (loop-agnostic!)
      while (1) {
          await $send->({
              type => 'sse.response.body',
              data => scalar(localtime()),
              # Optional fields:
              # event => 'clock',     # Event type
              # id => '123',          # Event ID
              # retry => 5000,        # Retry interval in ms
          });

          # Wait 1 second before next event
          await Future::IO->sleep(1);
      }
  }

  \&app;

Test it:

  # Terminal 1: Start server
  pagi-server clock.pl --port 5000

  # Terminal 2: Connect with curl
  curl http://localhost:5000/

  # Output (streaming):
  # data: Mon Dec 23 10:30:00 2025
  #
  # data: Mon Dec 23 10:30:01 2025
  #
  # data: Mon Dec 23 10:30:02 2025
  # ...

JavaScript client:

  const eventSource = new EventSource('http://localhost:5000/');
  eventSource.onmessage = (event) => {
      console.log('Time:', event.data);
  };

Key points:

=over 4

=item * Send C<sse.response.start> first with headers

=item * Send C<sse.response.body> events with C<data> field

=item * Optional fields: C<event> (event type), C<id> (event ID), C<retry> (reconnect interval)

=item * Connection stays open for streaming

=item * SSE is unidirectional (server to client only)

=back

See L<PAGI::SSE> for higher-level SSE utilities.

=head2 2.6 Streaming Responses

You can stream HTTP response bodies by sending multiple C<http.response.body> events:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use Future::IO;

  async sub app {
      my ($scope, $receive, $send) = @_;

      die "Expected http scope" unless $scope->{type} eq 'http';

      # Start response with chunked encoding
      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [
              ['content-type', 'text/plain'],
              ['transfer-encoding', 'chunked'],
          ],
      });

      # Send chunks over time
      for my $i (1..5) {
          await $send->({
              type => 'http.response.body',
              body => "Chunk $i\n",
              more => 1,  # More chunks coming
          });

          # Wait 1 second between chunks (loop-agnostic!)
          await Future::IO->sleep(1);
      }

      # Send final chunk (more => 0 is the default, so can be omitted)
      await $send->({
          type => 'http.response.body',
          body => "Done!\n",
      });
  }

  \&app;

Test it:

  curl http://localhost:5000/

  # Output (appears gradually):
  # Chunk 1
  # Chunk 2
  # Chunk 3
  # Chunk 4
  # Chunk 5
  # Done!

Key points:

=over 4

=item * Use C<transfer-encoding: chunked> or omit C<content-length>

=item * Set C<more =E<gt> 1> on all chunks except the last

=item * Omit C<more> on the final chunk (defaults to 0)

=item * Useful for large files, real-time data, or progressive rendering

=back

Streaming raises a question: what happens when you produce faster than the
client can read? The server buffers the overflow, and PAGI lets you B<see and
act on> that backlog through C<$scope-E<gt>{'pagi.transport'}> -- the
server-side analogue of the browser's C<WebSocket.bufferedAmount>.
C<buffered_amount> reports the bytes queued but not yet on the wire;
C<high_water_mark> and C<low_water_mark> bound the backpressure band; and the
C<on_high_water>/C<on_drain> callbacks let an event-driven producer pause and
resume. Use it to conflate a live feed -- skipping stale frames so a slow client
stays current -- or to throttle a source you cannot pace by awaiting. The handle
is optional: a server that cannot measure its buffer omits it, so guard for
C<undef>.

For a runnable demonstration see F<examples/13-flow-control>, the B<Flow Control>
recipes in L<PAGI::Cookbook>, and L<PAGI::Spec::Www/"Transport Flow Control"> for
the full interface.

=head2 2.7 Lifespan Protocol (Application Lifecycle)

The lifespan protocol allows your application to initialize resources on startup and clean them up on shutdown. The key feature is C<$scope-E<gt>{state}> - a hash for sharing resources (database pools, caches) from startup with every request in a worker process.

Each request scope receives a B<shallow copy> of this hash: the I<values> you store -- a database pool, a cache handle -- are shared by reference with every request, but the top-level keys are private to each copy. So store your shared resources B<once> at startup and use them through their references; do not expect a top-level key you assign B<in a request> (or reassign in the lifespan once requests are running) to be seen elsewhere. See L<PAGI::Spec::Lifespan/Lifespan State>.

B<Important:> In worker mode (C<--workers N>), each worker runs lifespan independently after forking. This means each worker initializes its own database connections, cache handles, etc. This is actually good - most database connections can't be shared across forks anyway.

  use strict;
  use warnings;
  use Future::AsyncAwait;

  async sub app {
      my ($scope, $receive, $send) = @_;

      # Handle lifespan events (startup then shutdown - a fixed sequence)
      if ($scope->{type} eq 'lifespan') {
          my $state = $scope->{state};  # store resources here; shared by reference with every request

          # Wait for startup event
          my $event = await $receive->();

          if ($event->{type} eq 'lifespan.startup') {
              eval {
                  # Store resources in $state - available to all requests
                  $state->{db} = MyDB->connect('dbi:Pg:dbname=myapp');
                  $state->{cache} = Cache::Memcached->new(servers => ['localhost:11211']);
                  warn "Application started successfully\n";
                  await $send->({ type => 'lifespan.startup.complete' });
              };
              if ($@) {
                  await $send->({ type => 'lifespan.startup.failed', message => $@ });
                  return;
              }
          }

          # Wait for shutdown event
          $event = await $receive->();

          if ($event->{type} eq 'lifespan.shutdown') {
              eval {
                  $state->{db}->disconnect if $state->{db};
                  $state->{cache}->disconnect_all if $state->{cache};
                  warn "Application shut down successfully\n";
                  await $send->({ type => 'lifespan.shutdown.complete' });
              };
              if ($@) {
                  await $send->({ type => 'lifespan.shutdown.failed', message => $@ });
              }
          }

          return;
      }

      # Handle HTTP requests
      if ($scope->{type} eq 'http') {
          my $state = $scope->{state};  # shallow copy of lifespan state; $db comes through by reference
          my $db = $state->{db};

          my $data = $db->selectrow_hashref('SELECT * FROM users LIMIT 1');

          await $send->({
              type => 'http.response.start',
              status => 200,
              headers => [['content-type', 'text/plain']],
          });

          await $send->({
              type => 'http.response.body',
              body => "Database connected: " . (defined $data ? 'yes' : 'no'),
          });
      }
  }

  \&app;

Key points:

=over 4

=item * Lifespan events are sent to the same application entry point

=item * Handle C<lifespan.startup> to initialize resources (database, cache, etc.)

=item * Handle C<lifespan.shutdown> to clean up resources

=item * Must respond with C<complete> or C<failed> for each event

=item * L<PAGI::Endpoint::Router> provides C<on_startup> and C<on_shutdown> callbacks to make this easier

=back

Example using L<PAGI::Endpoint::Router>:

  use PAGI::Endpoint::Router;

  my $db;

  my $router = PAGI::Endpoint::Router->new(
      on_startup => async sub {
          my ($scope) = @_;
          $db = MyDB->connect('dbi:Pg:dbname=myapp');
          warn "Database connected\n";
      },
      on_shutdown => async sub {
          my ($scope) = @_;
          $db->disconnect;
          warn "Database disconnected\n";
      },
  );

  # ... define routes ...

  $router->to_app;

=head2 2.8 Middleware: Wrapping an Application

Middleware is not a PAGI-Tools feature -- it is pure protocol. A middleware is
just a function that takes an application and returns a new application with the
same C<($scope, $receive, $send)> signature. Because the wrapper I<is> an
application, you can stack middleware as deeply as you like, and the server
cannot tell the difference between a bare app and one wrapped in ten layers.

=head3 The Basic Shape

  use Future::AsyncAwait;

  sub with_logging {
      my ($app) = @_;
      return async sub {
          my ($scope, $receive, $send) = @_;
          warn "--> $scope->{method} $scope->{path}\n";
          await $app->($scope, $receive, $send);
      };
  }

  # Wrap a bare app:
  my $wrapped = with_logging($app);

The outer C<sub> runs once, when you build the application; the inner
C<async sub> runs once per connection. Anything before C<< await $app->(...) >>
happens on the way in, anything after happens on the way out.

=head3 Modifying the Scope (Downward Only)

A middleware may add to C<$scope> for the layers below it, but it B<must not>
mutate the original in place. Clone first:

  sub with_request_id {
      my ($app) = @_;
      my $counter = 0;
      return async sub {
          my ($scope, $receive, $send) = @_;
          my $inner = { %$scope };               # shallow clone
          $inner->{'myapp.request_id'} = ++$counter;
          await $app->($inner, $receive, $send);
      };
  }

The clone is B<shallow>, and deliberately so. Top-level keys you set on the
clone are private to this layer and the layers below it; they never leak upward
to the parent layers or the server. But values reached I<through> a reference --
the C<pagi.connection> object, the lifespan C<state> namespace -- are shared by
every layer, because the clone copies the reference, not what it points at. The
rule of thumb: B<scope edits flow downward only; shared state lives behind a
reference.> See L<PAGI::Building/MIDDLEWARE> for the full sharing model and why
the clone is shallow rather than deep.

=head3 Wrapping C<$send> to Rewrite the Response

To observe or change what the inner app sends, wrap C<$send> and pass the
wrapper down:

  sub with_powered_by {
      my ($app) = @_;
      return async sub {
          my ($scope, $receive, $send) = @_;
          my $wrapped_send = async sub {
              my ($event) = @_;
              if ($event->{type} eq 'http.response.start') {
                  push @{ $event->{headers} }, [ 'x-powered-by', 'PAGI' ];
              }
              await $send->($event);
          };
          await $app->($scope, $receive, $wrapped_send);
      };
  }

This is how response-touching middleware (CORS headers, compression,
content-length) works: it intercepts the event stream as it flows out. Wrapping
C<$receive> works the same way for events flowing in -- buffer a request body,
decode a format, or fold in synthetic events.

=head3 Short-Circuiting

A middleware does not have to call the inner app at all. To reject a request,
respond yourself and return without calling C<$app>:

  sub require_api_key {
      my ($app) = @_;
      return async sub {
          my ($scope, $receive, $send) = @_;
          my %h = map { lc $_->[0] => $_->[1] } @{ $scope->{headers} // [] };
          unless (($h{'x-api-key'} // '') eq 'secret') {
              await $send->({
                  type    => 'http.response.start',
                  status  => 401,
                  headers => [ [ 'content-type', 'text/plain' ] ],
              });
              await $send->({ type => 'http.response.body', body => 'Unauthorized' });
              return;                              # never reach $app
          }
          await $app->($scope, $receive, $send);
      };
  }

=head3 Errors and the Return Value

A middleware B<must> let exceptions from the inner app propagate -- never
swallow them silently. If you catch in order to log, rethrow:

  my $result = eval { await $app->($scope, $receive, $send) };
  if (my $err = $@) {
      warn "app error: $err";
      die $err;                                   # propagate, do not swallow
  }
  return $result;

Note what that C<return> is for. The value an application resolves to is
B<inert>: the server assigns it no meaning (see
L<PAGI::Spec/"Application Completion Contract">). A middleware returns only to
propagate the inner app's B<Future> -- its completion and any exception. To
observe or rewrite the response, wrap C<$send>; never reach for the return
value. (Frameworks built on PAGI may use return values for their own internal
composition, but they convert them to events before handing control back to the
server.)

=head3 Stacking

Because each layer is itself an application, composing them is just nesting:

  my $app = require_api_key( with_request_id( with_logging( $bare_app ) ) );

The outermost wrapper runs first on the way in and last on the way out. Order
matters: put logging outside auth if you want to log rejected requests, inside
if you do not.

Everything above is plain protocol -- no toolkit required. PAGI-Tools ships
ready-made middleware (logging, sessions, CORS, compression, and more) and
ergonomic helpers for writing your own; each is built exactly like the wrappers
here. See L<PAGI::Tools::Tutorial>.

=head2 2.9 UTF-8 Handling

PAGI follows specific rules for UTF-8 encoding/decoding:

=head3 Request Paths

  $scope->{path}       # UTF-8 decoded string (use this for display/matching)
  $scope->{raw_path}   # Raw bytes (preserves original encoding)

PAGI uses Mojolicious-style path decoding: percent-encoded bytes are first
URL-decoded, then UTF-8 decoded. If UTF-8 decoding fails (invalid byte
sequences), the original URL-decoded bytes are preserved as-is.

Example:

  # URL: /users/%E4%B8%AD%E6%96%87
  $scope->{path} = '/users/中文'                    # (decoded)
  $scope->{raw_path} = '/users/%E4%B8%AD%E6%96%87'  # (raw bytes)

  # Invalid UTF-8: /users/%FF%FE
  $scope->{path} = '/users/\xFF\xFE'                # (falls back to bytes)
  $scope->{raw_path} = '/users/%FF%FE'              # (raw bytes)

=head3 Query Strings

Query strings are raw bytes. Use L<URI> to parse them properly:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use URI;
  use Encode qw(encode_utf8);

  async sub app {
      my ($scope, $receive, $send) = @_;

      die "Expected http scope" unless $scope->{type} eq 'http';

      # Use URI to parse query string (handles decoding automatically)
      my $uri = URI->new('?' . ($scope->{query_string} // ''));
      my %params = $uri->query_form;

      my $name = $params{name} // 'World';

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [['content-type', 'text/plain; charset=utf-8']],
      });

      await $send->({
          type => 'http.response.body',
          body => encode_utf8("Hello, $name!"),
      });
  }

  \&app;

=head3 Request Bodies

Request bodies are always raw bytes:

  my $event = await $receive->();
  my $raw_bytes = $event->{body};  # Raw bytes

  # If you know it's UTF-8 JSON:
  use Encode qw(decode_utf8);
  use JSON::MaybeXS;

  my $text = decode_utf8($raw_bytes);
  my $data = decode_json($text);

=head3 Response Bodies

Response bodies must be encoded to bytes:

  use Encode qw(encode_utf8);

  my $text = "Hello, 世界!";  # Perl string (characters)
  my $bytes = encode_utf8($text);  # Bytes

  await $send->({
      type => 'http.response.body',
      body => $bytes,  # Must be bytes, not characters
  });

=head3 Complete UTF-8 Example

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use Encode qw(encode_utf8 decode_utf8);
  use URI::Encode qw(uri_decode);

  async sub app {
      my ($scope, $receive, $send) = @_;

      die "Expected http scope" unless $scope->{type} eq 'http';

      # 1. Path is already decoded
      my $path = $scope->{path};  # UTF-8 string

      # 2. Query string needs decoding
      my $query = $scope->{query_string} // '';
      my ($key, $value) = split /=/, $query, 2;
      $value = decode_utf8(uri_decode($value // '')) if defined $value;

      # 3. Request body is raw bytes
      # NOTE: This loop does NOT block - await yields to event loop
      my $body_bytes = '';
      while (1) {
          my $event = await $receive->();
          $body_bytes .= $event->{body} if defined $event->{body};
          last unless $event->{more};
      }
      my $body_text = decode_utf8($body_bytes);  # Decode to string

      # 4. Build response (as string)
      my $response = "Path: $path\n";
      $response .= "Param: $value\n" if defined $value;
      $response .= "Body: $body_text\n" if length $body_text;

      # 5. Encode response to bytes
      my $response_bytes = encode_utf8($response);

      await $send->({
          type => 'http.response.start',
          status => 200,
          headers => [
              ['content-type', 'text/plain; charset=utf-8'],
              ['content-length', length($response_bytes)],
          ],
      });

      await $send->({
          type => 'http.response.body',
          body => $response_bytes,  # Bytes, not string
      });
  }

  \&app;

=head3 Using High-Level Helpers

L<PAGI::Request> and L<PAGI::Response> handle UTF-8 encoding/decoding automatically:

  use PAGI::Request;
  use PAGI::Response;

  async sub app {
      my ($scope, $receive, $send) = @_;

      die "Expected http scope" unless $scope->{type} eq 'http';

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

      # Path is already decoded
      my $path = $req->path;

      # Query params are automatically decoded
      my $name = $req->query_parameters->get('name') // 'World';

      # Body is automatically decoded (if content-type indicates UTF-8)
      my $body = await $req->body;

      # Response automatically encodes to UTF-8 bytes
      await $res->text("Hello, $name!");
  }

  \&app;

Key points:

=over 4

=item * C<$scope-E<gt>{path}> is decoded UTF-8; C<$scope-E<gt>{raw_path}> is bytes

=item * Query strings and request bodies are raw bytes (decode them yourself)

=item * Response bodies must be encoded to bytes before sending

=item * Use L<Encode> for explicit encoding/decoding

=item * L<PAGI::Request> and L<PAGI::Response> handle this automatically

=back

=head2 2.10 Async Patterns and Pitfalls

Understanding PAGI's async model is crucial to avoiding subtle bugs. This section covers common patterns and the most important pitfall to avoid.

=head3 The Completion Contract

B<Your app's returned Future signals "I'm done."> When your async sub returns (its Future resolves), the server considers the response complete and may:

=over 4

=item * Close the connection

=item * Write access logs

=item * Handle the next request (keep-alive)

=back

This means you B<must await all C<$send> calls> before your app returns.

=head3 The Most Common Mistake

This pattern looks reasonable but is B<broken>:

  # WRONG - DO NOT DO THIS
  async sub app {
      my ($scope, $receive, $send) = @_;

      # Schedule work for later without awaiting...
      Future::IO->sleep(1)->then(sub {
          $send->({
              type    => 'http.response.start',
              status  => 200,
              headers => [['content-type', 'text/plain']],
          });
          $send->({
              type => 'http.response.body',
              body => 'Hello after 1 second',
          });
      })->retain;  # Keep the future alive

      return;  # App returns immediately!
  }

B<What goes wrong:>

=over 4

=item 1. App returns immediately (Future resolves)

=item 2. Server thinks response is done (but nothing was sent!)

=item 3. The C<sleep> callback runs 1 second later

=item 4. By then, the connection may be closed or reused

=item 5. Result: empty response, corrupted response, or race condition

=back

B<Why C<-E<gt>retain> doesn't help:> The C<retain> method keeps the Future in memory, but the server doesn't know about it. The server only waits for your app's main Future - retained futures are invisible to it.

=head3 The Correct Pattern

Always await all response-affecting work:

  # CORRECT - await the delay
  async sub app {
      my ($scope, $receive, $send) = @_;

      # Wait for the delay
      await Future::IO->sleep(1);

      # Now send response (still within the app's Future)
      await $send->({
          type    => 'http.response.start',
          status  => 200,
          headers => [['content-type', 'text/plain']],
      });

      await $send->({
          type => 'http.response.body',
          body => 'Hello after 1 second',
      });
  }

=head3 Detecting This Problem

B<PAGI::Test::Client> will die with a clear error:

  App returned without sending response. Did you forget to 'await' your $send calls?

B<PAGI::Middleware::Lint> will warn (or die in strict mode):

  HTTP app completed without sending http.response.start. This usually means
  you forgot to 'await' your $send calls, or used ->retain for response-affecting work.

B<Enable Lint during development:>

  use PAGI::Middleware::Builder;

  my $app = builder {
      enable 'Lint', strict => 1;  # Die on violations
      $my_app;
  };

=head3 The Future Tree Mental Model

Think of your app's Futures as a tree:

  App Future (root)
  ├── await $send->({ response.start })
  ├── await $some_async_operation()
  │   └── await $send->({ response.body })
  └── (app returns when tree is complete)

Everything in the tree is awaited. The server waits for the root Future.

When you use C<-E<gt>retain>, you're creating a detached Future outside the tree:

  App Future (root)          Detached (invisible to server!)
  └── (returns immediately)   └── delay_future->then($send)

The server only sees the tree. Detached futures are orphaned.

=head3 A Scope Key Set in Middleware "Vanishes"

If you set a plain value on C<$scope> in one middleware and try to read it in
another, you may find it missing. Middleware clone the scope before modifying it,
and the clone is shallow: a top-level scalar set in one layer does not propagate
to the others. To share per-request state across layers on purpose, mutate
B<through a reference> (or use the C<stash>) rather than setting a top-level
scalar. The full model -- and why it works this way -- is in
L<PAGI::Building/"Scope is cloned per layer, not shared">.

=head3 Summary

=over 4

=item * B<Always await> C<$send> calls and any async work that affects the response

=item * B<Never use C<-E<gt>retain>> for response-affecting work

=item * B<Use Lint middleware> to catch mistakes early

=item * C<-E<gt>retain> is only safe B<after> the response is completely sent

=back

=head1 BEYOND THE PROTOCOL: OPTIONAL CONVENIENCES

Everything above is the PAGI protocol: the complete contract between an async
application and an async server. You can build any PAGI application with
nothing more than what you have seen here: an C<async sub>, C<$scope>,
C<$receive>, C<$send>, and L<Future::AsyncAwait>.

The C<PAGI-Tools> distribution provides B<optional> convenience helpers on top
of the protocol: middleware (logging, sessions, CORS, compression, and more),
request and response sugar (L<PAGI::Request>, L<PAGI::Response>), WebSocket and
SSE helpers, routers, and ready-made applications. None of it is required; it
exists to save boilerplate when you want it. Every piece is an ordinary PAGI
application or a plain wrapper around the C<$scope>/C<$receive>/C<$send>
protocol, so you can read, replace, or ignore any of it.

For a guide to those helpers, see L<PAGI::Tools::Tutorial>.

=head1 PART 3: REFERENCE

=head2 3.1 Scope Reference

The C<$scope> hashref contains connection metadata:

=head3 Async Operations Without Loop Access

For async operations like delays, use L<Future::IO> which is loop-agnostic:

  use Future::IO;
  await Future::IO->sleep(5);  # Works with any event loop

This keeps your application code portable across different PAGI server
implementations.

=head3 Common Fields (All Types)

  $scope->{type}         # 'http', 'websocket', 'sse', 'lifespan'

=head3 HTTP Scope

  $scope->{type}         # 'http'
  $scope->{method}       # 'GET', 'POST', etc.
  $scope->{path}         # URL path (decoded UTF-8)
  $scope->{raw_path}     # URL path (raw bytes)
  $scope->{query_string} # Query string (raw bytes)
  $scope->{headers}      # Arrayref of [name, value] pairs
  $scope->{scheme}       # 'http' or 'https'
  $scope->{http_version} # '1.0' or '1.1'
  $scope->{client}       # [ip, port] of client
  $scope->{server}       # [ip, port] of server

=head3 WebSocket Scope

  $scope->{type}         # 'websocket'
  $scope->{path}         # URL path
  $scope->{query_string} # Query string
  $scope->{headers}      # Request headers
  $scope->{subprotocols} # Arrayref of requested subprotocols
  $scope->{client}       # [ip, port] of client

=head3 SSE Scope

  $scope->{type}         # 'sse'
  $scope->{path}         # URL path
  $scope->{query_string} # Query string
  $scope->{headers}      # Request headers
  $scope->{client}       # [ip, port] of client

=head3 Lifespan Scope

  $scope->{type}         # 'lifespan'

=head2 3.2 Event Reference

=head3 HTTP Events

Receive:

  { type => 'http.request', body => $bytes, more => 1 }
  { type => 'http.disconnect' }

Send:

  { type => 'http.response.start', status => 200, headers => [...] }
  { type => 'http.response.body', body => $bytes, more => 0 }

=head3 WebSocket Events

Receive:

  { type => 'websocket.connect' }
  { type => 'websocket.receive', text => $text }
  { type => 'websocket.receive', bytes => $bytes }
  { type => 'websocket.disconnect', code => 1000, reason => '' }

Send:

  { type => 'websocket.accept', subprotocol => 'optional' }
  { type => 'websocket.send', text => $text }
  { type => 'websocket.send', bytes => $bytes }
  { type => 'websocket.close', code => 1000, reason => '' }

=head3 SSE Events

Receive:

  { type => 'sse.connect' }
  { type => 'sse.disconnect' }

Send:

  { type => 'sse.response.start', status => 200, headers => [...] }
  { type => 'sse.response.body', data => $text, event => 'name', id => '1' }

=head3 Lifespan Events

Receive:

  { type => 'lifespan.startup' }
  { type => 'lifespan.shutdown' }

Send:

  { type => 'lifespan.startup.complete' }
  { type => 'lifespan.startup.failed', message => $error }
  { type => 'lifespan.shutdown.complete' }
  { type => 'lifespan.shutdown.failed', message => $error }

=head1 NEXT STEPS

You've covered the PAGI protocol! For more information:

=over 4

=item * L<PAGI::Cookbook> - Worked, runnable recipes for each protocol feature

=item * L<PAGI::PSGI> - if you're coming from PSGI

=item * L<PAGI::Building> - if you're building a framework on PAGI

=item * Browse F<examples/> for complete working applications, including a small web framework built on PAGI

=item * Read the specification: L<PAGI::Spec> and the protocol documents L<PAGI::Spec::Www>, L<PAGI::Spec::Lifespan>, L<PAGI::Spec::Server>.

=item * L<PAGI::Tools::Tutorial> - Guide to the optional convenience helpers (middleware, routers, request/response sugar, ready-made apps)

=back

=head1 SEE ALSO

=over 4

=item * L<PAGI> - The PAGI specification distribution and overview

=item * L<PAGI::Cookbook> - Worked, runnable recipes for the protocol

=item * L<PAGI::PSGI> - Coming to PAGI from PSGI

=item * L<PAGI::Building> - Building frameworks on PAGI

=item * L<PAGI::Spec> - The full PAGI specification

=item * L<PAGI::Server> - The reference server (C<PAGI-Server> distribution)

=item * L<PAGI::Tools::Tutorial> - Guide to optional helpers (C<PAGI-Tools> distribution)

=item * L<Future::AsyncAwait> - Async/await syntax for Perl

=item * L<Future::IO> - Loop-agnostic async primitives (sleep, I/O)

=item * L<PAGI::EventLoops> - event loops, non-blocking I/O, and binding a server to a loop

=back

=head1 AUTHOR

PAGI Contributors

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2025 by the PAGI contributors.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
