=pod

=encoding utf-8

=head1 NAME

PAGI::Cookbook - Recipes for Common PAGI Tasks

=head1 DESCRIPTION

This cookbook provides ready-to-use solutions for common web development tasks
with PAGI. Each recipe is self-contained and can be adapted to your needs.

B<Prerequisites:> This cookbook assumes you've read the L<PAGI::Tutorial>,
particularly Parts 1-3 covering the raw PAGI protocol, middleware, and helper
classes.

=head1 ROUTING RECIPES

These recipes cover advanced routing patterns using L<PAGI::App::Router> and
L<PAGI::Endpoint::Router>. For basic routing, see the Tutorial's Built-in
Applications section.

=head2 Path Parameters and Wildcards

Use C<:param> syntax for named parameters in routes:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Request;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  # Single parameter
  $router->get('/users/:id' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      # Access path parameter via $req->path_param()
      my $id = $req->path_param('id');

      await $res->json({ id => $id, name => 'User ' . $id });
  });

  # Multiple parameters
  $router->get('/posts/:post_id/comments/:comment_id' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      my $post_id = $req->path_param('post_id');
      my $comment_id = $req->path_param('comment_id');

      await $res->json({
          post_id => $post_id,
          comment_id => $comment_id,
      });
  });

  $router->to_app;

Parameters are stored in C<$scope-E<gt>{path_params}> and accessed via C<$req-E<gt>path_param($name)>.

=head3 Wildcard Routes

Use C<*name> syntax to capture multiple path segments:

  my $router = PAGI::App::Router->new;

  # Wildcard captures everything after /files/
  $router->get('/files/*path' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      # Access wildcard via $req->path_param()
      my $path = $req->path_param('path');

      # /files/docs/readme.txt -> path = 'docs/readme.txt'
      # /files/images/logo.png -> path = 'images/logo.png'

      await $res->text("File path: $path");
  });

  $router->to_app;

Wildcards match one or more path segments, while named parameters (C<:param>) match a single segment.

=head2 WebSocket and SSE Routes

Route WebSocket connections by path:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::WebSocket;

  my $router = PAGI::App::Router->new;

  # WebSocket echo server
  $router->websocket('/ws' => async sub {
      my ($scope, $receive, $send) = @_;
      my $ws = PAGI::WebSocket->new($scope, $receive, $send);

      await $ws->accept;

      await $ws->each_text(async sub {
          my ($text) = @_;
          await $ws->send_text("Echo: $text");
      });
  });

  # WebSocket with path parameters
  $router->websocket('/ws/chat/:room' => async sub {
      my ($scope, $receive, $send) = @_;
      my $ws = PAGI::WebSocket->new($scope, $receive, $send);

      # Access room parameter
      my $room = $ws->stash->{'pagi.params'}{room};

      await $ws->accept;
      await $ws->send_text("Joined room: $room");

      # ... chat logic ...
  });

  $router->to_app;

=head3 SSE Routes

Route Server-Sent Events by path:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::SSE;

  my $router = PAGI::App::Router->new;

  # SSE clock
  $router->sse('/events' => async sub {
      my ($scope, $receive, $send) = @_;
      my $sse = PAGI::SSE->new($scope, $receive, $send);

      await $sse->start;

      await $sse->every(1, async sub {
          await $sse->send_event("Time: " . time);
      });
  });

  # SSE with path parameters
  $router->sse('/events/:channel' => async sub {
      my ($scope, $receive, $send) = @_;
      my $sse = PAGI::SSE->new($scope, $receive, $send);

      # Access channel parameter
      my $channel = $sse->stash->{'pagi.params'}{channel};

      await $sse->start;
      await $sse->send_event("Subscribed to: $channel");

      # ... streaming logic ...
  });

  $router->to_app;

=head2 Route-Level Middleware

Apply middleware to specific routes by passing an arrayref:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Request;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  # Define middleware
  my $auth_mw = async sub {
      my ($scope, $receive, $send, $next) = @_;

      # Check authorization
      my $token = '';
      for my $h (@{$scope->{headers}}) {
          if (lc($h->[0]) eq 'authorization') {
              $token = $h->[1];
              last;
          }
      }

      unless ($token eq 'Bearer secret123') {
          my $res = PAGI::Response->new($scope, $send);
          await $res->status(401)->json({ error => 'Unauthorized' });
          return;  # Don't call $next->()
      }

      # Authorized - continue to handler
      await $next->();
  };

  # Apply middleware to specific routes
  $router->get('/admin' => [$auth_mw] => async sub {
      my ($scope, $receive, $send) = @_;
      my $res = PAGI::Response->new($scope, $send);
      await $res->text('Admin panel');
  });

  # Multiple middleware
  my $log_mw = async sub {
      my ($scope, $receive, $send, $next) = @_;
      warn "Request: $scope->{method} $scope->{path}\n";
      await $next->();
  };

  $router->post('/admin/users' => [$log_mw, $auth_mw] => async sub {
      my ($scope, $receive, $send) = @_;
      # ... handler ...
  });

  $router->to_app;

Middleware are executed in order (C<$log_mw> runs before C<$auth_mw> in the example above).

=head2 Nested Routers and Mounting

Use C<mount()> to attach sub-applications or routers at a prefix:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Request;
  use PAGI::Response;

  # Create API router
  my $api_router = PAGI::App::Router->new;
  $api_router->get('/users' => async sub {
      my ($scope, $receive, $send) = @_;
      my $res = PAGI::Response->new($scope, $send);
      await $res->json([{ id => 1, name => 'Alice' }]);
  });

  # Create main router
  my $main_router = PAGI::App::Router->new;

  # Mount API router at /api prefix
  # /api/users will be handled by $api_router
  $main_router->mount('/api' => $api_router->to_app);

  # Mount with middleware (applies to all sub-routes)
  my $cors_mw = async sub {
      my ($scope, $receive, $send, $next) = @_;
      # Add CORS headers...
      await $next->();
  };

  $main_router->mount('/api' => [$cors_mw] => $api_router->to_app);

  $main_router->to_app;

=head2 Class-Based Routing with PAGI::Endpoint::Router

L<PAGI::Endpoint::Router> provides an object-oriented approach to routing. You define a class that extends C<PAGI::Endpoint::Router>, implement a C<routes()> method, and use method names as handlers.

=head3 Basic Router Class

  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  # Define routes
  sub routes {
      my ($self, $r) = @_;

      $r->get('/' => 'home');
      $r->get('/about' => 'about');
      $r->post('/contact' => 'contact');
  }

  # Handler methods receive ($self, $req, $res) for HTTP
  async sub home {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Welcome!</h1>');
  }

  async sub about {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>About Us</h1>');
  }

  async sub contact {
      my ($self, $req, $res) = @_;
      my $data = await $req->json;
      # ... process contact form ...
      await $res->json({ success => 1 });
  }

  1;

  # In app.pl:
  # use MyApp;
  # MyApp->to_app;

Handler methods are called with:

=over 4

=item * C<$self> - Router instance (access C<$self-E<gt>state> for per-instance data)

=item * C<$req> - L<PAGI::Request> object (already constructed)

=item * C<$res> - L<PAGI::Response> object (already constructed)

=back

=head3 WebSocket Handlers

WebSocket handlers receive C<($self, $ws)>:

  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  sub routes {
      my ($self, $r) = @_;

      $r->get('/' => 'home');
      $r->websocket('/ws' => 'handle_ws');
      $r->websocket('/ws/chat/:room' => 'handle_chat');
  }

  async sub home {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Home</h1>');
  }

  # WebSocket handler receives ($self, $ws)
  async sub handle_ws {
      my ($self, $ws) = @_;

      await $ws->accept;

      await $ws->each_text(async sub {
          my ($text) = @_;
          await $ws->send_text("Echo: $text");
      });
  }

  # WebSocket with path parameters
  async sub handle_chat {
      my ($self, $ws) = @_;

      # Access path parameter via $ws->path_param()
      my $room = $ws->path_param('room');

      await $ws->accept;
      await $ws->send_text("Joined room: $room");

      # ... chat logic ...
  }

  1;

=head3 SSE Handlers

SSE handlers receive C<($self, $sse)>:

  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  sub routes {
      my ($self, $r) = @_;

      $r->get('/' => 'home');
      $r->sse('/events' => 'handle_events');
      $r->sse('/events/:channel' => 'handle_channel');
  }

  async sub home {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Home</h1>');
  }

  # SSE handler receives ($self, $sse)
  async sub handle_events {
      my ($self, $sse) = @_;

      await $sse->start;

      await $sse->every(1, async sub {
          await $sse->send_event("Time: " . time);
      });
  }

  # SSE with path parameters
  async sub handle_channel {
      my ($self, $sse) = @_;

      # Access path parameter
      my $channel = $sse->scope->{'pagi.params'}{channel};

      await $sse->start;
      await $sse->send_event("Channel: $channel");

      # ... streaming logic ...
  }

  1;

=head3 Lifecycle Hooks

Override C<on_startup> and C<on_shutdown> for application lifecycle management:

  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  # Called when application starts
  async sub on_startup {
      my ($self) = @_;

      # Initialize resources
      $self->state->{db} = DBI->connect('dbi:SQLite:dbname=app.db');
      $self->state->{started_at} = time();

      warn "Application started\n";
  }

  # Called when application shuts down
  async sub on_shutdown {
      my ($self) = @_;

      # Clean up resources
      $self->state->{db}->disconnect if $self->state->{db};

      warn "Application shut down\n";
  }

  sub routes {
      my ($self, $r) = @_;

      $r->get('/' => 'home');
  }

  async sub home {
      my ($self, $req, $res) = @_;

      # Access state initialized in on_startup
      my $db = $self->state->{db};
      my $uptime = time() - $self->state->{started_at};

      await $res->json({
          uptime => $uptime,
          database => defined($db) ? 'connected' : 'disconnected',
      });
  }

  1;

Note: C<$self-E<gt>state> is per-worker in multi-worker mode. For shared state, use an external store (Redis, database, etc.).

=head3 Route-Level Middleware with Method Names

Define middleware methods and reference them by name:

  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  sub routes {
      my ($self, $r) = @_;

      $r->get('/' => 'home');

      # Apply middleware by method name
      $r->get('/admin' => ['require_auth'] => 'admin_page');

      # Multiple middleware
      $r->post('/admin/users' => ['log_request', 'require_auth'] => 'create_user');
  }

  # Middleware method signature: ($self, $req, $res, $next)
  async sub require_auth {
      my ($self, $req, $res, $next) = @_;

      my $token = $req->header('authorization');

      unless ($token && $token eq 'Bearer secret123') {
          await $res->status(401)->json({ error => 'Unauthorized' });
          return;  # Don't call $next->()
      }

      # Authorized - continue to handler
      await $next->();
  }

  async sub log_request {
      my ($self, $req, $res, $next) = @_;

      warn "Request: " . $req->method . " " . $req->path . "\n";

      await $next->();
  }

  async sub home {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Home</h1>');
  }

  async sub admin_page {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Admin Panel</h1>');
  }

  async sub create_user {
      my ($self, $req, $res) = @_;
      my $data = await $req->json;
      # ... create user ...
      await $res->status(201)->json({ success => 1 });
  }

  1;

Middleware methods receive C<($self, $req, $res, $next)> and must call C<await $next-E<gt>()> to continue to the handler.

=head3 Mounting Sub-Routers

Create modular applications by mounting sub-routers:

  # lib/MyApp/API.pm
  package MyApp::API;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  sub routes {
      my ($self, $r) = @_;

      $r->get('/users' => 'list_users');
      $r->get('/users/:id' => 'get_user');
      $r->post('/users' => 'create_user');
  }

  async sub list_users {
      my ($self, $req, $res) = @_;
      await $res->json([
          { id => 1, name => 'Alice' },
          { id => 2, name => 'Bob' },
      ]);
  }

  async sub get_user {
      my ($self, $req, $res) = @_;
      my $id = $req->path_param('id');
      await $res->json({ id => $id, name => "User $id" });
  }

  async sub create_user {
      my ($self, $req, $res) = @_;
      my $data = await $req->json;
      await $res->status(201)->json({ id => 3, name => $data->{name} });
  }

  1;

  # lib/MyApp.pm
  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;
  use MyApp::API;

  sub routes {
      my ($self, $r) = @_;

      $r->get('/' => 'home');

      # Mount API router at /api prefix
      $r->mount('/api' => MyApp::API->to_app);
  }

  async sub home {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Home</h1>');
  }

  1;

  # app.pl
  # use MyApp;
  # MyApp->to_app;

Now C</api/users> and C</api/users/:id> are handled by C<MyApp::API>.

=head3 Complete Example

Here's a complete working example demonstrating all features:

  package MyApp;
  use parent 'PAGI::Endpoint::Router';
  use strict;
  use warnings;
  use Future::AsyncAwait;

  async sub on_startup {
      my ($self) = @_;
      $self->state->{started} = time();
      $self->state->{users} = [
          { id => 1, name => 'Alice' },
          { id => 2, name => 'Bob' },
      ];
  }

  sub routes {
      my ($self, $r) = @_;

      # HTTP routes
      $r->get('/' => 'home');
      $r->get('/users' => 'list_users');
      $r->get('/users/:id' => 'get_user');
      $r->post('/users' => 'create_user');

      # Protected route
      $r->get('/admin' => ['require_auth'] => 'admin');

      # WebSocket
      $r->websocket('/ws' => 'ws_echo');

      # SSE
      $r->sse('/events' => 'events');
  }

  async sub require_auth {
      my ($self, $req, $res, $next) = @_;
      my $token = $req->header('authorization');
      return await $res->status(401)->json({ error => 'Unauthorized' })
          unless $token && $token eq 'Bearer secret';
      await $next->();
  }

  async sub home {
      my ($self, $req, $res) = @_;
      await $res->html('<h1>Welcome to MyApp</h1>');
  }

  async sub list_users {
      my ($self, $req, $res) = @_;
      await $res->json($self->state->{users});
  }

  async sub get_user {
      my ($self, $req, $res) = @_;
      my $id = $req->path_param('id');
      my ($user) = grep { $_->{id} == $id } @{$self->state->{users}};
      return await $res->status(404)->json({ error => 'Not found' })
          unless $user;
      await $res->json($user);
  }

  async sub create_user {
      my ($self, $req, $res) = @_;
      my $data = await $req->json;
      my $user = { id => scalar(@{$self->state->{users}}) + 1, name => $data->{name} };
      push @{$self->state->{users}}, $user;
      await $res->status(201)->json($user);
  }

  async sub admin {
      my ($self, $req, $res) = @_;
      await $res->json({ message => 'Admin access granted' });
  }

  async sub ws_echo {
      my ($self, $ws) = @_;
      await $ws->accept;
      await $ws->each_text(async sub {
          my ($text) = @_;
          await $ws->send_text("Echo: $text");
      });
  }

  async sub events {
      my ($self, $sse) = @_;
      await $sse->start;
      await $sse->every(1, async sub {
          await $sse->send_json({ time => time });
      });
  }

  1;

Run it:

  # app.pl
  use MyApp;
  MyApp->to_app;

  # Terminal:
  pagi-server app.pl --port 5000

Test it:

  # HTTP
  curl http://localhost:5000/
  curl http://localhost:5000/users
  curl http://localhost:5000/users/1
  curl -X POST http://localhost:5000/users -H 'Content-Type: application/json' -d '{"name":"Charlie"}'

  # Protected route
  curl http://localhost:5000/admin
  curl -H 'Authorization: Bearer secret' http://localhost:5000/admin

  # WebSocket (JavaScript)
  const ws = new WebSocket('ws://localhost:5000/ws');
  ws.onmessage = (e) => console.log(e.data);
  ws.send('Hello!');

  # SSE (JavaScript)
  const sse = new EventSource('http://localhost:5000/events');
  sse.onmessage = (e) => console.log(JSON.parse(e.data));

See C<examples/endpoint-router-demo/> for more examples.

=head1 AUTHENTICATION & SESSIONS

=head2 Session Management

Use L<PAGI::Middleware::Session> for session management:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Middleware::Session;
  use PAGI::Request;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  # Add session middleware
  $router->use(PAGI::Middleware::Session->new(
      secret => 'your-secret-key-here',
      cookie_name => 'session_id',
      expires => 86400,  # 1 day
  ));

  $router->get('/login' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      # Set session data
      $req->session->{user_id} = 123;
      $req->session->{username} = 'alice';

      await $res->text('Logged in!');
  });

  $router->get('/profile' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      # Read session data
      my $username = $req->session->{username} // 'Guest';

      await $res->text("Hello, $username!");
  });

  $router->get('/logout' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      # Clear session
      $req->session({});

      await $res->text('Logged out!');
  });

  $router->to_app;

=head2 Basic Authentication

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Middleware::BasicAuth;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  # Protect routes with Basic Auth
  $router->use(PAGI::Middleware::BasicAuth->new(
      realm => 'Admin Area',
      authenticator => sub {
          my ($username, $password) = @_;
          return $username eq 'admin' && $password eq 'secret';
      },
  ));

  $router->get('/' => async sub {
      my ($scope, $receive, $send) = @_;
      my $res = PAGI::Response->new($scope, $send);
      await $res->text('Protected content');
  });

  $router->to_app;

=head2 Bearer Token (JWT)

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Middleware::Auth::Bearer;
  use PAGI::Request;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  # Verify JWT tokens
  $router->use(PAGI::Middleware::Auth::Bearer->new(
      validate => sub {
          my ($token) = @_;
          # Return user data if valid, undef if not
          return decode_jwt($token);  # Your JWT decode logic
      },
  ));

  $router->get('/api/me' => async sub {
      my ($scope, $receive, $send) = @_;
      my $req = PAGI::Request->new($scope, $receive);
      my $res = PAGI::Response->new($scope, $send);

      # User data from validated token
      my $user = $req->stash->{user};

      await $res->json({ user => $user });
  });

  $router->to_app;

=head1 REAL-TIME PATTERNS

=head2 PubSub for WebSocket Chat

For real-time features with multiple WebSocket clients:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::WebSocket;
  use PAGI::Server::PubSub;

  my $pubsub = PAGI::Server::PubSub->new;
  my $router = PAGI::App::Router->new;

  $router->websocket('/chat/:room' => async sub {
      my ($scope, $receive, $send) = @_;
      my $ws = PAGI::WebSocket->new($scope, $receive, $send);
      my $room = $scope->{'pagi.params'}{room};

      await $ws->accept;

      # Subscribe to room
      my $sub = $pubsub->subscribe($room, sub {
          my ($message) = @_;
          $ws->try_send_text($message);
      });

      # Broadcast received messages
      await $ws->each_text(sub {
          my ($text) = @_;
          $pubsub->publish($room, $text);
      });

      # Unsubscribe on disconnect
      $pubsub->unsubscribe($room, $sub);
  });

  $router->to_app;

B<Important:> L<PAGI::Server::PubSub> is in-memory only and works within a single process. For multi-worker or multi-server deployments, use Redis pub/sub or a message broker.

See C<examples/websocket-chat/> for a complete chat application example.

=head2 SSE Dashboard

Server-Sent Events are ideal for real-time dashboards:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::SSE;

  my $router = PAGI::App::Router->new;

  $router->get('/events' => async sub {
      my ($scope, $receive, $send) = @_;

      my $sse = PAGI::SSE->new($scope, $receive, $send);
      await $sse->start;

      # Enable keepalive to prevent timeout
      $sse->keepalive(15);

      # Send updates every second
      await $sse->every(1, async sub {
          await $sse->send_json({
              cpu => rand(100),
              memory => rand(100),
              requests => int(rand(1000)),
              timestamp => time(),
          }, event => 'metrics');
      });
  });

  $router->to_app;

JavaScript client:

  const events = new EventSource('/events');

  events.addEventListener('metrics', (e) => {
      const data = JSON.parse(e.data);
      document.getElementById('cpu').textContent = data.cpu.toFixed(1) + '%';
      document.getElementById('memory').textContent = data.memory.toFixed(1) + '%';
  });

See C<examples/sse-dashboard/> for a complete dashboard example.

=head1 BACKGROUND WORK

Web requests should respond quickly. Long-running work should be done in the background.

=head2 Fire-and-Forget Async I/O

For non-blocking operations (async HTTP calls, async database queries),
use C<< ->on_fail() >> to handle errors, then C<< ->retain() >> to prevent
"lost future" warnings:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::App::Router;
  use PAGI::Response;

  my $router = PAGI::App::Router->new;

  # Simulated async email service
  async sub send_welcome_email {
      my ($email) = @_;
      # In reality: await $http_client->post_async($email_api, ...);
      await IO::Async::Loop->new->delay_future(after => 2);
      warn "Email sent to $email\n";
  }

  $router->post('/signup' => async sub {
      my ($scope, $receive, $send) = @_;
      my $res = PAGI::Response->new($scope, $send);

      # Respond immediately
      await $res->status(201)->json({ status => 'created' });

      # Fire-and-forget with error handling
      # IMPORTANT: Always add on_fail() before retain() to avoid silent failures
      send_welcome_email('user@example.com')
          ->on_fail(sub { warn "Email send failed: @_" })
          ->retain();
  });

  $router->to_app;

B<Warning:> Using C<< ->retain() >> alone silently swallows errors. Always
add C<< ->on_fail() >> to log or handle failures.

B<Note:> If you're writing middleware or server extensions that inherit from
L<IO::Async::Notifier>, prefer C<< $self->adopt_future($f) >> instead of
C<< ->retain() >>. The C<adopt_future> method properly tracks futures and
propagates errors to the notifier's error handling.

=head2 CPU-Bound Work in Subprocess

For blocking or CPU-intensive work, use L<IO::Async::Function> to run in a child process:

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use IO::Async::Function;
  use IO::Async::Loop;
  use PAGI::App::Router;
  use PAGI::Response;

  my $loop = IO::Async::Loop->new;

  # Worker runs code in subprocess
  my $pdf_worker = IO::Async::Function->new(
      code => sub {
          my ($data) = @_;
          # This blocking code runs in a CHILD PROCESS
          # It doesn't block the main event loop
          sleep 5;  # Simulate heavy work
          return "PDF generated for $data";
      },
  );
  $loop->add($pdf_worker);

  my $router = PAGI::App::Router->new;

  $router->post('/generate-pdf' => async sub {
      my ($scope, $receive, $send) = @_;
      my $res = PAGI::Response->new($scope, $send);

      # Option A: Wait for result (blocks this request only)
      my $result = await $pdf_worker->call(args => ['report']);
      await $res->json({ result => $result });

      # Option B: Fire-and-forget (respond immediately)
      # my $f = $pdf_worker->call(args => ['report']);
      # $f->on_done(sub { warn "PDF ready: $_[0]\n" });
      # $f->on_fail(sub { warn "PDF failed: $_[0]\n" });
      # $f->retain();
      # await $res->json({ status => 'processing' });
  });

  $router->to_app;

B<Warning:> IO::Async::Function forks child processes. If running with C<--workers N>, each server worker forks its own Function workers. With 4 server workers and 2 Function workers each, you have 12 processes. For high-volume CPU work, consider a dedicated job queue instead.

See C<examples/background-tasks/> for complete working examples.

=head1 FORMS & UPLOADS

=head2 URL-Encoded Forms

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::Request;
  use PAGI::Response;

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

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

      if ($req->method eq 'POST') {
          my $form = await $req->form;
          my $username = $form->{username};
          my $password = $form->{password};

          # Validate and process...
          await $res->redirect('/dashboard');
      }
      else {
          await $res->html(<<'HTML');
  <form method="POST">
      <input name="username" placeholder="Username">
      <input name="password" type="password" placeholder="Password">
      <button type="submit">Login</button>
  </form>
  HTML
      }
  }

  \&app;

=head2 File Uploads (Multipart)

  use strict;
  use warnings;
  use Future::AsyncAwait;
  use PAGI::Request;
  use PAGI::Response;

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

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

      if ($req->method eq 'POST') {
          my $uploads = await $req->uploads;

          for my $upload (@$uploads) {
              my $filename = $upload->filename;
              my $size = $upload->size;
              my $type = $upload->content_type;

              # Save to disk
              $upload->save_to('/uploads/' . $filename);

              # Or read content
              # my $content = $upload->content;
          }

          await $res->json({ uploaded => scalar(@$uploads) });
      }
      else {
          await $res->html(<<'HTML');
  <form method="POST" enctype="multipart/form-data">
      <input type="file" name="file" multiple>
      <button type="submit">Upload</button>
  </form>
  HTML
      }
  }

  \&app;

See C<examples/10-forms-and-uploads/> for complete examples.

=head1 DEPLOYMENT

=head2 TLS/HTTPS

L<PAGI::Server> supports TLS natively:

  pagi-server app.pl --port 443 \
      --tls-cert /path/to/cert.pem \
      --tls-key /path/to/key.pem

For development, generate a self-signed certificate:

  openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
      -days 365 -nodes -subj '/CN=localhost'

  pagi-server app.pl --port 3443 \
      --tls-cert cert.pem --tls-key key.pem

Then access via C<https://localhost:3443> (browser will warn about self-signed cert).

See C<examples/11-tls-https/> for more TLS configuration options.

=head1 TESTING

Test PAGI apps directly without a running server:

  use strict;
  use warnings;
  use Test2::V0;
  use Future::AsyncAwait;

  # Load your app
  my $app = require './app.pl';

  # Test helper to create scope and capture response
  async sub test_request {
      my (%opts) = @_;

      my $scope = {
          type         => 'http',
          method       => $opts{method} // 'GET',
          path         => $opts{path} // '/',
          query_string => $opts{query} // '',
          headers      => $opts{headers} // [],
      };

      my @body_parts = defined $opts{body} ? ($opts{body}) : ();
      my $body_sent = 0;

      my $receive = async sub {
          if (@body_parts) {
              return {
                  type => 'http.request',
                  body => shift @body_parts,
                  more => scalar(@body_parts) > 0,
              };
          }
          return { type => 'http.disconnect' };
      };

      my %response = (status => undef, headers => [], body => '');
      my $send = async sub {
          my ($event) = @_;
          if ($event->{type} eq 'http.response.start') {
              $response{status} = $event->{status};
              $response{headers} = $event->{headers};
          }
          elsif ($event->{type} eq 'http.response.body') {
              $response{body} .= $event->{body} // '';
          }
      };

      await $app->($scope, $receive, $send);
      return \%response;
  }

  # Tests
  subtest 'GET /' => async sub {
      my $res = await test_request(path => '/');
      is $res->{status}, 200, 'status is 200';
      like $res->{body}, qr/Hello/, 'body contains Hello';
  };

  subtest 'POST /api/users' => async sub {
      my $res = await test_request(
          method => 'POST',
          path => '/api/users',
          headers => [['content-type', 'application/json']],
          body => '{"name":"Alice"}',
      );
      is $res->{status}, 201, 'status is 201';
  };

  done_testing;

=head1 SEE ALSO

=over 4

=item * L<PAGI::Tutorial> - Learn PAGI from the ground up

=item * L<PAGI::App::Router> - Functional routing

=item * L<PAGI::Endpoint::Router> - Class-based routing

=item * L<PAGI::Middleware::Session> - Session management

=item * L<PAGI::Server::PubSub> - In-memory pub/sub

=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
