DEV Community

John Napiorkowski
John Napiorkowski

Posted on

PAGI 0.002002: Clarifying How Applications Are Loaded

Introduction

PAGI 0.002002 is a specification release. It does not change the runtime shape of a PAGI application. A running application is still one asynchronous code reference:

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

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

    await $send->({
        type => 'http.response.body',
        body => 'Hello from PAGI!',
        more => 0,
    });
}
Enter fullscreen mode Exit fullscreen mode

What the release clarifies is how a general-purpose runner may reach that runtime boundary.

The gap between Python callables and Perl objects

PAGI's scope, receive, and send runtime shape was inspired by ASGI. ASGI describes an application as an asynchronous callable receiving those three values. In Python, that callable may be a function or an instantiated object implementing __call__; the distinction disappears at the call site.

Perl does not normally treat a blessed object as a code reference. We could approximate Python with callable-object overloading, but making overload magic part of the gateway contract would obscure a useful boundary. PSGI and Plack already give Perl a more familiar convention: use objects while configuring an application, then produce a code reference for runtime dispatch.

PAGI 0.002002 now makes that convention explicit. A general-purpose application runner should accept either:

  • a native PAGI code reference; or
  • an already-instantiated object providing to_app.

For example, an application component may retain configuration while it is being constructed:

package MyApp;

use Future::AsyncAwait;

sub new {
    my ($class, %args) = @_;
    return bless { greeting => $args{greeting} // 'Hello' }, $class;
}

sub to_app {
    my ($self) = @_;

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

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

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

# app.pl
package main;

MyApp->new(greeting => 'Hello from an application provider');
Enter fullscreen mode Exit fullscreen mode

A runner adopting this convention calls to_app exactly once for that loaded instance, verifies that it returned a code reference, and uses only that code reference for connection dispatch. It does not call to_app for every request.

This matters for more than performance. Route compilation, middleware construction, and application setup happen at a predictable boundary. A broken provider fails while the application is loading rather than after the server begins accepting traffic. The server, middleware, and application still communicate through one uniform runtime interface.

Deliberately not magical

The provider must be an instantiated object. Neither of these is an application provider under the new convention:

'MyApp'          # A package-name string
MyApp->to_app    # Class-method normalization
Enter fullscreen mode Exit fullscreen mode

A command-line runner may accept a module name as part of its own discovery interface, but it must construct the object—or otherwise obtain a native application code reference—before PAGI provider normalization begins. This keeps module loading, constructor arguments, and plugin policy out of the gateway protocol.

Blessed code references remain code references and are dispatched directly. Their code-reference nature takes precedence over a to_app method.

Application lifecycle is also unchanged. Startup and shutdown belong to the PAGI lifespan protocol. Methods on a provider object do not silently become lifecycle hooks.

A clearer server boundary

The accompanying server integration specification no longer requires every conforming server class to implement a particular new(app => ...) constructor and run method. A server might be a class, function, embedded service, or external process. Conformance is about how it invokes the normalized application and exchanges PAGI events, not the shape of its configuration API.

This separates three responsibilities that had previously been too easy to conflate:

  1. Deployment tooling discovers and constructs an application.
  2. A provider object is normalized once through to_app.
  3. The server dispatches only the resulting PAGI code reference.

The next step is to bring the reference PAGI runner into line with this clarification. This release defines the portable boundary first; it does not claim that every existing runner already accepts provider objects.

One more clarification: what $send completion means

PAGI 0.002002 also gathers the $send Future contract into one place. Applications should continue to await every send:

await $send->({
    type => 'http.response.body',
    body => $chunk,
    more => 1,
});
Enter fullscreen mode Exit fullscreen mode

Successful resolution means the server has validated and consumed the event, finished with any application-owned resource tied to it, and accepted the output into its outbound processing path. It does not mean the client has received the bytes. A server may keep the Future pending to apply backpressure, so awaiting $send is how an application self-paces without blocking the Perl thread.

Versioning

The distribution version is 0.002002. The core PAGI specification advances to draft 0.4, while the server integration specification is draft 0.2. WWW and Lifespan remain at draft 0.3. Core and protocol specifications now explicitly evolve independently; an omitted protocol spec_version uses that protocol's documented compatibility default rather than borrowing the core version.

The result is a small but important cleanup: PAGI keeps one minimal runtime protocol while giving Perl applications a conventional, explicit way to carry configuration and structure up to the point where runtime dispatch begins.

The complete change is available in PAGI pull request #60.

Top comments (0)