=encoding UTF-8

=head1 NAME

PAGI::Cookbook - worked, runnable recipes for the PAGI protocol

=head1 DESCRIPTION

A gallery of complete recipes for "how do I do X in PAGI?". Where
L<PAGI::Tutorial> teaches the protocol as a linear path, the cookbook is for
dipping in: each recipe is a self-contained application you can read top to
bottom and run.

Every recipe is B<raw PAGI> -- a single C<async sub> over C<$scope>,
C<$receive>, and C<$send>, with no helper modules -- so what you see is the
protocol itself, not a framework on top of it. Each one corresponds to a
runnable application under the distribution's F<examples/> directory.

=head2 The shape of an application

A PAGI application is a file that returns an C<async sub>. Here is the smallest
complete one, in full:

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

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

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

    $app;   # the server loads this file and uses the returned coderef

That is the complete F<examples/01-hello-http> application. The recipes below
show the C<async sub> for each scenario; wrap any of them in the same header and
trailing C<$app;> to get a runnable file. Recipes that sleep also need
C<use Future::IO;>, and a couple need C<use Encode;> -- noted where relevant.

=head2 Running a recipe

You need a PAGI server; these instructions use C<pagi-server> from the
C<PAGI-Server> distribution, but any conforming server works:

    pagi-server --app examples/05-sse-broadcaster/app.pl --port 5000

=head1 START HERE: A FRAMEWORK ON PAGI

I<Example: F<examples/mini-framework>>

The best way to understand PAGI's value is to see how little stands between the
protocol and a real web framework. F<examples/mini-framework> defines C<Nano> --
routing, path parameters, and genuinely non-blocking async dispatch -- in about
fifty lines of plain Perl, and an application that uses it never touches the
protocol at all:

    my $app = Nano->new;

    $app->get('/' => sub { "Hello from a web framework built on PAGI!\n" });

    $app->get('/hello/:name' => sub {
        my ($req) = @_;
        "Hello, $req->{params}{name}!\n";
    });

    # An async handler awaits slow work without blocking other requests:
    $app->get('/slow/:secs' => async sub {
        my ($req) = @_;
        await Future::IO->sleep( $req->{params}{secs} );
        "Done.\n";
    });

    $app->to_app;   # turn the routes into a PAGI app

Read F<examples/mini-framework/README.md> for the full walkthrough of how C<Nano>
is built -- it is the single best argument for building on PAGI.

=head1 HTTP

=head2 Hello, HTTP

I<Example: F<examples/01-hello-http>>

The complete app is shown above under L</The shape of an application>. The two
things to take away: an HTTP response is one C<http.response.start> event
followed by one or more C<http.response.body> events, and C<< more => 0 >> marks
the final body. Header names are lower-case, and bodies are bytes (encode text
yourself -- see L</UTF-8 round trips>).

=head2 Reading a request body

I<Example: F<examples/03-request-body>>

The request body arrives as a stream of C<http.request> events. Keep calling
C<$receive> until one has C<< more >> false, accumulating as you go:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

        my $body = '';
        while (1) {
            my $event = await $receive->();
            last if $event->{type} ne 'http.request';   # e.g. http.disconnect
            $body .= $event->{body} // '';
            last unless $event->{more};                 # more => 0 ends the body
        }

        my $message = length $body ? "You sent: $body" : "No body provided";

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

The body bytes are raw and undecoded; the application decides how to interpret
them. Even when you do not need the body, drain it before responding.

=head2 Streaming a response

I<Example: F<examples/02-streaming-response>> (needs C<use Future::IO;>)

To stream, send several C<http.response.body> events with C<< more => 1 >> and a
final one with C<< more => 0 >>. Declaring C<< trailers => 1 >> on the start
event lets you finish with an C<http.response.trailers> event. Hold a pending
C<$receive> Future and check C<is_ready> so you stop early if the client leaves:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

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

        my @chunks     = ( "Chunk 1\n", "Chunk 2\n", "Chunk 3\n" );
        my $disconnect = $receive->();   # completes when http.disconnect arrives

        for my $i (0 .. $#chunks) {
            last if $disconnect->is_ready;          # client went away
            my $more = ($i < $#chunks) ? 1 : 0;
            await $send->({ type => 'http.response.body', body => $chunks[$i], more => $more });
            await Future::IO->sleep(1) if $more;    # pace the stream
        }

        await $send->({
            type    => 'http.response.trailers',
            headers => [ [ 'x-stream-complete', '1' ] ],
        });
    };

=head2 UTF-8 round trips

I<Example: F<examples/12-utf8>> (needs C<use Encode qw(decode_utf8 encode_utf8);>)

PAGI is precise about where text is decoded for you and where you get raw bytes.
This complete handler echoes a C<?text=...> query parameter back with a
character count:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

        # $scope->{path} is already UTF-8-decoded per the spec. The query string
        # is raw bytes: percent-decode it, then decode UTF-8 yourself.
        my $text = '';
        if ( ( $scope->{query_string} // '' ) =~ /text=([^&]*)/ ) {
            my $raw = $1;
            $raw =~ s/%([0-9A-Fa-f]{2})/chr hex $1/eg;
            $text = decode_utf8($raw);
        }

        my $reply = "You sent: $text (" . length($text) . " characters)\n";
        my $bytes = encode_utf8($reply);   # responses go out as bytes

        await $send->({
            type    => 'http.response.start',
            status  => 200,
            headers => [
                [ 'content-type',   'text/plain; charset=utf-8' ],
                [ 'content-length', length $bytes ],   # BYTE count, not characters
            ],
        });
        await $send->({ type => 'http.response.body', body => $bytes, more => 0 });
    };

The full example wraps this in an HTML page that also exercises the path and a
form body. See the UTF-8 sections of L<PAGI::Spec::Www> and L<PAGI> for the
complete rules.

=head2 Detecting client disconnect

When a client goes away mid-response -- closes the tab, drops the network -- you
want to stop work, not keep streaming into a dead connection. Every scope type
delivers a disconnect event (C<http.disconnect>, C<websocket.disconnect>,
C<sse.disconnect>) to C<$receive>, so the universal way to notice is to receive
it. Note that sending after a disconnect is simply a no-op -- it never raises --
so you detect disconnection through these mechanisms, not by checking the result
of C<$send>.

For B<HTTP>, PAGI additionally gives you a connection-state object at
C<< $scope->{'pagi.connection'} >>. It exists because calling C<$receive> on an
HTTP scope just to poll for disconnect would consume request-body data; the
object lets you check without touching the queue. It offers a cheap synchronous
check, a cleanup callback, and a Future you can race against:

    use Future;
    use Future::IO;

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

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

        $conn->on_disconnect(sub { warn "client went away\n" });   # fires only on an abnormal drop

        for my $i (1 .. 100) {
            last unless $conn->is_connected;   # cheap check before each step
            await $send->({ type => 'http.response.body', body => "line $i\n", more => 1 });
            await Future::IO->sleep(1);
        }
        await $send->({ type => 'http.response.body', body => '', more => 0 });
    };

To cancel a long-running task the moment the client leaves, race it against
C<< $conn->disconnect_future >> -- C<wait_any> resolves as soon as either Future
completes, and cancels the other:

    my $result = await Future->wait_any( $expensive_work, $conn->disconnect_future );

C<on_disconnect> fires B<only> when something goes wrong -- the client drops, a
timeout fires, the server aborts. Its counterpart C<on_complete> fires B<only>
when the response was fully delivered. Exactly one of the two runs per request,
so they are the natural place to put rollback-vs-commit or abort-vs-finalize
logic without juggling a flag yourself:

    my $txn = $db->begin_work;
    $conn->on_complete(sub   { $txn->commit   });   # response fully delivered
    $conn->on_disconnect(sub { $txn->rollback });   # client left, or aborted

B<WebSocket> and B<SSE> do not get a C<pagi.connection> object: their C<$receive>
queues carry messages, not a body you must preserve, so awaiting the
C<websocket.disconnect> / C<sse.disconnect> event is already the clean way to
notice -- as the WebSocket and SSE recipes do. (The C<PAGI::WebSocket> and
C<PAGI::SSE> helper objects in C<PAGI-Tools> wrap this for you.)

The full connection-state interface (C<is_connected>, C<disconnect_reason>,
C<on_disconnect>, C<on_complete>, C<disconnect_future>) is specified in
L<PAGI::Spec::Www/"Connection State">.

=head1 WEBSOCKET

=head2 Echo server

I<Example: F<examples/04-websocket-echo>>

A WebSocket scope opens with a C<websocket.connect> event that you answer with
C<websocket.accept>; then you loop, receiving C<websocket.receive> frames and
sending C<websocket.send>, until C<websocket.disconnect>. Unlike HTTP, one scope
spans the whole connection:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'websocket';

        my $event = await $receive->();
        die "Expected websocket.connect" if $event->{type} ne 'websocket.connect';
        await $send->({ type => 'websocket.accept' });

        while (1) {
            my $frame = await $receive->();
            if ($frame->{type} eq 'websocket.receive') {
                if (defined $frame->{text}) {
                    await $send->({ type => 'websocket.send', text => "echo: $frame->{text}" });
                }
                elsif (defined $frame->{bytes}) {
                    await $send->({ type => 'websocket.send', bytes => $frame->{bytes} });
                }
            }
            elsif ($frame->{type} eq 'websocket.disconnect') {
                last;
            }
        }
    };

=head2 Rejecting a handshake with a custom response

Sending C<websocket.close> before C<websocket.accept> rejects a handshake with a
bare C<403>. To reject with a B<custom> response -- a C<401> with a
C<WWW-Authenticate> header, a C<429>, a redirect -- send a
C<websocket.http.response.start> + C<websocket.http.response.body> pair instead
of C<websocket.accept>, when the server advertises the C<websocket.http.response>
extension:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'websocket';

        await $receive->();   # websocket.connect

        if (!authorized($scope)) {                       # your auth check
            if ($scope->{extensions}{'websocket.http.response'}) {
                await $send->({
                    type    => 'websocket.http.response.start',
                    status  => 401,
                    headers => [ [ 'content-type',     'application/json' ],
                                 [ 'www-authenticate', 'Bearer' ] ],
                });
                await $send->({
                    type => 'websocket.http.response.body',
                    body => '{"error":"unauthorized"}',
                });
            }
            else {
                await $send->({ type => 'websocket.close' });   # bare 403 fallback
            }
            return;
        }

        await $send->({ type => 'websocket.accept' });
        # ... normal session ...
    };

These events are valid only before C<websocket.accept>: the server writes the
HTTP response and closes, so the WebSocket never opens. The C<PAGI::WebSocket>
helper in C<PAGI-Tools> wraps this as
C<< $ws->deny(status => 401, headers => [...], body => ...) >>, with the same
fallback. See L<PAGI::Spec::Www/"WebSocket Denial Response (extension)">.

=head1 SERVER-SENT EVENTS

=head2 Broadcasting events

I<Example: F<examples/05-sse-broadcaster>> (needs C<use Future::IO;>)

Open the stream with C<sse.start>, then push C<sse.send> events, each a
C<< { event => ..., data => ... } >>. Hold a C<$receive> Future and check it so
you stop when the client closes:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'sse';

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

        my $disconnect = $receive->();   # completes on sse.disconnect

        my @events = (
            { event => 'tick', data => '1' },
            { event => 'tick', data => '2' },
            { event => 'done', data => 'finished' },
        );

        for my $msg (@events) {
            last if $disconnect->is_ready;
            await Future::IO->sleep(1);
            await $send->({ type => 'sse.send', %$msg });
        }
    };

Using C<< Future::IO->sleep >> rather than C<sleep> keeps the handler
non-blocking and loop-agnostic.

=head2 Ending a stream: return vs C<sse.close>

A stream ends in one of two ways, and they are B<identical on the wire> -- the
server writes the end-of-stream marker and closes; the client cannot tell them
apart. B<Return> after the final C<sse.send> and the server ends the stream when
your handler's Future resolves:

    for my $msg (@events) {
        await $send->({ type => 'sse.send', %$msg });
    }
    return;   # server flushes buffered events and closes

Or end it B<explicitly> with C<sse.close>. This ends the stream B<immediately --
before the handler returns> -- so a helper deep in the call stack can stop the
stream in place, and you can attach a server-side C<reason> for your logs and
metrics. The C<reason> is never sent to the client (SSE has no close frame):

    async sub stream_until_done {
        my ($send, $source) = @_;
        while (my $item = await $source->next) {
            return await $send->({ type => 'sse.close', reason => 'source_error' })
                if $item->{fatal};
            await $send->({ type => 'sse.send', data => $item->{data} });
        }
        await $send->({ type => 'sse.close', reason => 'source_drained' });
    }

After C<sse.close>, any further C<sse.send>/C<sse.comment>/C<sse.keepalive>
raises, and a second C<sse.close> is a no-op. To tell the I<client> anything --
including "stop reconnecting" -- send an ordinary C<sse.send> event before
closing (and answer the client's next connection with HTTP C<204>); C<reason> is
for the server side only.

Because the close is an event on C<$send>, middleware can observe it. Wrap
C<$send> to fan close reasons out to metrics or tracing without touching the
application:

    sub observe_sse_closes {
        my ($app) = @_;
        return async sub {
            my ($scope, $receive, $send) = @_;
            my $wrapped = async sub {
                my ($event) = @_;
                metric_inc('sse_close', { reason => $event->{reason} // 'unspecified' })
                    if ($event->{type} // '') eq 'sse.close';
                return await $send->($event);   # forward unchanged
            };
            await $app->($scope, $receive, $wrapped);
        };
    }

An auto-close-on-return stream has no event to intercept, so it cannot be
observed this way. See L<PAGI::Spec::Www/"Close SSE - C<send> event">.

=head1 FLOW CONTROL

=head2 Keeping a slow client current instead of stale

I<Example: F<examples/13-flow-control>> (the same pattern over SSE, easy to run with C<curl>)

Every streaming scope -- C<http>, C<websocket>, and C<sse> -- may carry a
flow-control handle at C<< $scope->{'pagi.transport'} >>. It reports how many
bytes the server has queued for the client but B<not yet written to the
network>, so you can make your own delivery decisions instead of only blocking
until the buffer drains. It is the server-side analogue of the browser's
C<WebSocket.bufferedAmount> -- a primitive many async stacks do not expose at
all.

The handle is optional: a server that cannot measure its outbound buffer omits
it, so guard for C<undef>. The first use is B<conflation> -- on a high-frequency
feed, skip an update when the client is already behind, because it will get the
next, fresher one. A sparse-but-current feed beats a dense-but-lagging one:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'websocket';

        await $receive->();                          # websocket.connect
        await $send->({ type => 'websocket.accept' });

        my $transport = $scope->{'pagi.transport'};  # undef if unsupported

        while (1) {
            my $update = await next_update();        # your high-frequency feed

            # Skip this tick if the client is falling behind. Threshold relative
            # to the server's own ceiling, not a hard-coded byte count.
            if ($transport) {
                my $ceiling = $transport->high_water_mark // 65536;
                next if $transport->buffered_amount > $ceiling / 2;
            }

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

C<buffered_amount> is a synchronous, non-blocking read; C<high_water_mark> and
C<low_water_mark> report the band at which the server engages and releases
backpressure. (The C<PAGI-Tools> helpers wrap the same handle as
C<< $ws->buffered_amount >> and C<< $ws->is_writable >>.)

=head2 Pausing a producer you do not control

Conflation works when B<you> drive the loop and can choose to skip. But
sometimes an external source -- a pub/sub subscription, a database change feed --
pushes events B<at> you faster than a slow client drains, and you cannot
self-pace by awaiting C<$send>. For that the handle offers two edge-triggered
callbacks: C<on_high_water> fires once when the buffer crosses up to the high
mark, and C<on_drain> fires once when it falls back below the low mark. Use them
to pause and resume the source:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'sse';

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

        my $feed = subscribe_to_feed();         # your external event source
        $feed->on_event(sub {
            my ($data) = @_;
            $send->({ type => 'sse.send', data => $data });   # not awaited
        });

        # The feed ignores the client's pace, so let the transport throttle it:
        # pause when the buffer fills, resume when it drains. Each fires once per
        # crossing, then re-arms.
        my $transport = $scope->{'pagi.transport'};
        if ($transport) {
            $transport->on_high_water(sub { $feed->pause });
            $transport->on_drain(sub     { $feed->resume });
        }

        await $receive->();   # park here until sse.disconnect
        $feed->cancel;
    };

The two callbacks form a hysteresis cycle -- pause at the high mark, resume at
the low mark, with a gap between so a buffer hovering near one threshold does not
flap. They are the push-model counterpart to the synchronous reads: reach for
the reads when you drive the loop, the callbacks when something else does.

The full interface (C<buffered_amount>, C<high_water_mark>, C<low_water_mark>,
C<on_high_water>, C<on_drain>) is specified in
L<PAGI::Spec::Www/"Transport Flow Control">.

=head1 APPLICATION LIFECYCLE

=head2 Lifespan and shared state

I<Example: F<examples/06-lifespan-state>>

The C<lifespan> scope runs once per process, around startup and shutdown. Reply
to each event, and use it to build state that later requests read from
C<< $scope->{state} >>. This complete app handles both scope types:

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

        if ($scope->{type} eq 'lifespan') {
            my $state = $scope->{state} //= {};
            while (1) {
                my $event = await $receive->();
                if ($event->{type} eq 'lifespan.startup') {
                    $state->{greeting} = 'Hello from lifespan';
                    await $send->({ type => 'lifespan.startup.complete' });
                }
                elsif ($event->{type} eq 'lifespan.shutdown') {
                    await $send->({ type => 'lifespan.shutdown.complete' });
                    last;
                }
            }
            return;
        }

        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

        # Drain the request body, then reply using the shared state.
        while (1) {
            my $event = await $receive->();
            last if $event->{type} ne 'http.request';
            last unless $event->{more};
        }

        my $greeting = ( $scope->{state} // {} )->{greeting} // 'Hello';
        await $send->({
            type    => 'http.response.start',
            status  => 200,
            headers => [ [ 'content-type', 'text/plain' ] ],
        });
        await $send->({ type => 'http.response.body', body => "$greeting via shared state", more => 0 });
    };

Not every server or run drives lifespan, so an app should still work when the
startup event never arrives (hence the C<// {}> and C<// 'Hello'> fallbacks).

Each request gets a B<shallow copy> of C<state>, so this read-only pattern -- set
a value at startup, read it in requests -- is exactly what it is built for. To
share something B<mutable>, store a single object or container at startup and
mutate it through that shared reference; never reassign a top-level C<state> key
from a request, which changes only that request's copy. See
L<PAGI::Spec::Lifespan/Lifespan State>.

=head1 SERVER EXTENSIONS

=head2 Forcing a flush: the fullflush extension

I<Example: F<examples/07-extension-fullflush>>

Servers advertise optional capabilities in C<< $scope->{extensions} >>; always
check for one before using it. C<fullflush> pushes buffered bytes to the client
immediately:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

        my $supports_fullflush = exists $scope->{extensions}{fullflush};

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

        my @chunks = ( "Line 1\n", "Line 2\n", "Line 3\n" );
        for my $i (0 .. $#chunks) {
            my $is_last = ( $i == $#chunks );
            await $send->({ type => 'http.response.body', body => $chunks[$i], more => $is_last ? 0 : 1 });
            await $send->({ type => 'http.fullflush' }) if $supports_fullflush && !$is_last;
        }
    };

The extension mechanism is described in L<PAGI::Spec::Extensions>.

=head2 Inspecting the TLS connection

I<Example: F<examples/08-tls-introspection>>

When the connection is over TLS and the server supports the extension,
C<< $scope->{extensions}{tls} >> carries the connection's security metadata; the
key is absent for cleartext connections:

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

        my $tls  = $scope->{extensions}{tls};
        my $body = $tls
            ? sprintf("TLS version 0x%04x; client cert: %s\n",
                      $tls->{tls_version} // 0, $tls->{client_cert_name} // '(none)')
            : "Connection is not using TLS\n";

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

The full example pretty-prints more fields (cipher suite, certificate chain) as
JSON. See L<PAGI::Spec::Tls> for the complete set.

=head1 PUTTING IT TOGETHER

=head2 A real-time job runner

I<Example: F<examples/11-job-runner>>

The capstone example is too large to inline -- it spans several modules -- but
it is worth reading once you are comfortable with the recipes above. It combines
all three application protocols in one app sharing a single job queue: a REST
API over B<HTTP> to create and cancel jobs, per-job progress over B<SSE>, and a
live queue dashboard over B<WebSocket>. It is the most complete demonstration of
raw PAGI.

Note: it drives C<IO::Async> timers directly, so it runs only under an
IO::Async-based server such as PAGI::Server -- see its F<README.md> for how it
could be made loop-agnostic with L<Future::IO>.

=head1 INTEROP

=head2 Running a PSGI app under PAGI

You do not have to rewrite a PSGI application to use PAGI. Wrap it once with
L<PAGI::App::WrapPSGI> (from the C<PAGI-Tools> distribution) --
C<< PAGI::App::WrapPSGI->new( psgi_app => $app )->to_app >> -- and it runs on
any conforming PAGI server, with native async, WebSocket, or SSE handlers
dispatched beside it. A runnable demo ships with C<PAGI-Tools> at
F<examples/09-psgi-bridge>. See L<PAGI::PSGI> for the full migration guide.

=head1 SEE ALSO

=over

=item *

L<PAGI::Tutorial> - the protocol taught as a linear path

=item *

L<PAGI::PSGI> - coming to PAGI from PSGI

=item *

L<PAGI::Building> - building a framework on PAGI

=item *

L<PAGI> - the specification overview and ecosystem map

=item *

L<PAGI::Spec> - the formal specification

=item *

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

=back

=head1 AUTHOR

John Napiorkowski E<lt>jjnapiork@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

This software is licensed under the same terms as Perl itself.

=cut
