package Catalyst::Seal;

use strict;
use warnings;

our $VERSION = '0.04';

use XSLoader ();
XSLoader::load('Catalyst::Seal', $VERSION);

use Catalyst::Seal::Guard ();

our @STEPS;

sub register_step {
    my ($name, $code) = @_;
    push @STEPS, { name => $name, code => $code };
    return;
}

our $DEBUG = $ENV{CATALYST_SEAL_DEBUG} ? 1 : 0;

our @NOTES;
our @FAILURES;

sub note {
    my ($msg) = @_;
    push @NOTES, $msg;
    warn "Catalyst::Seal: $msg\n" if $DEBUG;
    return;
}

sub notes    { @NOTES }
sub failures { @FAILURES }

sub enabled {
    return 0 if defined $ENV{CATALYST_SEAL} && !$ENV{CATALYST_SEAL};
    return 1;
}

require Catalyst::Seal::Immutable;
require Catalyst::Seal::Exceptions;
require Catalyst::Seal::Modifiers;
require Catalyst::Seal::ClassData;
require Catalyst::Seal::Accessors;
require Catalyst::Seal::Prepare;
require Catalyst::Seal::Dispatch;
require Catalyst::Seal::Execute;
require Catalyst::Seal::Finalize;
require Catalyst::Seal::Construct;
require Catalyst::Seal::Middleware;

my %WANTED;
my %SEALED;
my $HOOKED = 0;

sub import {
    my ($class) = @_;
    my $app = caller;
    return unless enabled();
    $WANTED{$app} = 1;
    _install_hook();
    return;
}

sub _install_hook {
    return if $HOOKED++;
    require Catalyst;
    my $orig = Catalyst->can('setup_finalize')
        or return note('Catalyst has no setup_finalize, nothing sealed');
    no strict 'refs';
    no warnings 'redefine';
    *Catalyst::setup_finalize = sub {
        my @ret = $orig->(@_);
        my $app = ref $_[0] || $_[0];
        __PACKAGE__->seal($app) if delete $WANTED{$app};
        return wantarray ? @ret : $ret[0];
    };
    return;
}

sub seal {
    my ($class, $app) = @_;
    $app = ref $app || $app;
    return unless enabled();
    return if $SEALED{$app}++;

    for my $step (@STEPS) {
        my $ok = eval { $step->{code}->($app); 1 };
        next if $ok;
        my $err = $@;
        push @FAILURES, { step => $step->{name}, error => $err, app => $app };
        warn "Catalyst::Seal: step '$step->{name}' failed for $app: $err";
    }

    if ($DEBUG) {
        warn sprintf "Catalyst::Seal: sealed %s, %d step(s), %d note(s), %d failure(s)\n",
            $app, scalar @STEPS, scalar @NOTES, scalar @FAILURES;
    }
    return;
}

1;

__END__

=head1 NAME

Catalyst::Seal - freeze a Catalyst application at setup and make it 5x faster

=head1 VERSION

Version 0.04

=cut

=head2 seal

    Catalyst::Seal->seal('MyApp');

Runs every registered step against the application class. Called for you from
C<setup_finalize>; call it directly only if you are sealing an application that
did not C<use Catalyst::Seal>.

Each step runs in its own eval. A step that dies is a bug in this distribution,
so it always warns rather than disappearing into the hook, and the remaining
steps still run.

=cut

=head1 SYNOPSIS

    package MyApp;

    use Catalyst::Seal;
    use Catalyst qw/ ConfigLoader Static::Simple /;

    __PACKAGE__->setup();

=head1 DESCRIPTION

After C<setup_finalize> a Catalyst application is frozen: the class tree, the
configuration, the action table and the attribute layouts all stop changing.
Catalyst goes on re-deriving those facts from the metaobject protocol on every
request. Catalyst::Seal makes one pass at the end of C<setup()> and compiles the
frozen facts into constant form.

No application code changes. Place the C<use> line above C<use Catalyst> and
nothing else.

=head2 What this module does

=over 4

=item * Makes the application class and its components immutable, which Catalyst
does for controllers but not for the application class, even though the
application class is also the per-request context class.

=item * Replaces the two L<Try::Tiny> blocks on the request path with plain
C<eval>.

=item * Replaces every C<mk_classdata> accessor with an XS constant. The stock
one calls C<Moose::Util::find_meta> on every read to answer a question that
stopped changing at C<setup_finalize>: 84 of those calls per request on a bare
application. A write unseals the accessor and puts the original back.

=item * Takes C<find_meta> out of the C<Catalyst::Component::config> read path.

=item * Installs the composed body of every method carrying a C<before>,
C<after> or C<around> modifier directly, in place of the trampoline
L<Class::MOP::Method::Wrapped> installs, which calls C<set_subname> on every
invocation to attach a name that has not changed since the modifier was applied.
Ten of those are on the request path. A modifier added after the seal puts the
trampoline back.

=item * Short-circuits the L<Catalyst::Response> guard that warns about setting
a header after the headers were finalised, which on a read evaluates three
accessors to reach a condition that already could not be true.

=item * Seals C<$c-E<gt>config> as a constant. Catalyst croaks on any write to
it once setup has finished, so after setup it cannot change, and the whole
C<around>, C<find_meta>, C<get_or_add_package_symbol> chain behind it is dead
weight on every one of the nine reads a request makes.

=item * Replaces the attribute readers on the context, request and response
classes with XS. Only readers, only on immutable classes, and only where the
reader is Moose's own. A lazy attribute whose slot is not built yet delegates to
that reader, so the builders stay Moose's and a C<predicate> keeps telling the
truth.

=item * Aliases C<req>, C<res> and C<comp> straight to the methods they
delegate to, removing a whole frame from each of 25 calls a request.

=item * Memoises the two action lookups the forward chain repeats on every
request. One action costs five forwards, each of which turns a string like
C<'/foo/_BEGIN'> into an action object by walking a table that stopped changing
at C<setup_finalize>. The chain itself is not flattened: the private steps go on
C<$c-E<gt>stack>, and C<$c-E<gt>depth> is what gates the C<detach> and C<go>
rethrows, so a flat chain would have to reimplement C<execute> to keep them
honest.

=item * Memoises the encoding decision. C<finalize_encoding> spends 49 us per
request deciding that a C<text/plain> body does not need encoding, by calling
C<content_type> four times and C<content_type_charset> three times to re-parse
one header string. The answer is a pure function of the raw content type, the
content encoding, the encodable-type pattern and the application encoding.

=item * Replaces the L<Try::Tiny> in Moose's inlined destructor for
L<Catalyst::Response> with an C<eval>. Two closures were being built on every
response destroyed, to call a four line C<DEMOLISH>.

=item * Replaces the L<Try::Tiny> in C<_handle_param_unicode_decoding> with an
C<eval>. Every query parameter, body parameter and path argument is decoded
through it, name and value separately, so a four parameter query string builds
sixteen closures to decode eight strings. Worth 15 us on such a request, which
is more than the parsing it surrounds.

=item * Builds the L<HTTP::Headers> hash in C<prepare_headers> directly rather
than through one C<header> call per environment key. The spelling of each field
is asked of HTTP::Headers once, the first time a request carries it, and
remembered, so nothing here has to know which headers HTTP::Headers considers
standard.

=item * Parses the query string in C. The stock parser builds a decoder
closure, a L<Hash::MultiValue> and a regex before it has looked at the query,
then runs the name and the value of every pair through C<unescape_uri> and
C<_handle_param_unicode_decoding> separately. Four pairs cost 12.9 us that way
and 1.3 us this way; an empty query string, which cannot cost anything, cost
1.9. The UTF-8 the C decoder accepts is exactly the UTF-8 Encode accepts,
noncharacters and all, and anything it is not certain of goes back to Encode
to be refused in Encode's own words.

=item * Places the headers whose spelling is already known in C, one pass over
the environment. 1.5 us to 0.9, on top of the 4.2 the stock body costs.

=item * Replaces the constructors for the context, request and response classes
with XS built from a template resolved at seal time: which key each attribute
reads, whether it has a default and what kind, and which of them Moose would do
more than a store for. 8.0 us per request to 4.3. A value for a typed attribute
is put to the constraint's own check, and anything that fails it - or a trigger,
a coercion, a required attribute with nothing to fall back on, a subclass, an
odd argument list - takes the stock constructor entire.

=item * Hoists the accessor traffic out of C<Catalyst::execute>, which spends
nine accessor calls before it does anything and is called nine times a request,
and puts C<Catalyst::Action::execute> and C<::dispatch> in XS. Both of those
exist only to reach a slot: five accessor frames between them, nine times each.

=item * Answers a write through a sealed accessor in XS as well as a read,
where the attribute is a plain one: 132 ns to 51.

=item * Seals C<use_stats>, which C<setup_stats> decided and C<execute> asks
twenty times a request, and puts C<depth> - C<scalar @{ shift-E<gt>stack }> -
in XS.

=item * Skips C<URI::canonical> in C<prepare_path> when the URI just built is
already canonical, which is one regex to decide and three authority parses to
ask. Checked against L<URI> itself at seal time, positive and negative, rather
than against its source.

=item * Stops a controller's C<BUILD> firing two lazy builders on the
per-request context object. The application class inherits
L<Catalyst::Controller>, so C<BUILDALL> runs its C<BUILD> on every context
object, to compute values derived entirely from class data.

=item * Defers building the L<Catalyst::Stats> object when stats are disabled.
Its C<tree> attribute is required and not lazy, so stock builds a
L<Tree::Simple> and reads the clock on every request for an object nothing then
reads. With stats enabled nothing changes, because that timestamp is the request
start.

=back

On a bare application, 279.1 to 70.6 us per request - measured as the best of
five runs of four thousand requests, sealed and unsealed, in the same process
as one another.

The throughput figures below are 0.03's and have not been re-run since:

    cat-hello              6586 req/s
    cat-seal              20461 req/s

=head2 Environment

=over 4

=item C<CATALYST_SEAL=0>

Hard kill switch. Nothing is sealed and the application is stock Catalyst.

=item C<CATALYST_SEAL_DEBUG=1>

Report what was sealed, and what was skipped and why.

=back

=head1 AUTHOR

LNATION <email@lnation.org>

=head1 LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut
