File Coverage

blib/lib/PAGI/Lifespan.pm
Criterion Covered Total %
statement 87 88 98.8
branch 26 32 81.2
condition 21 35 60.0
subroutine 14 15 93.3
pod 4 9 44.4
total 152 179 84.9


line stmt bran cond sub pod time code
1             package PAGI::Lifespan;
2             $PAGI::Lifespan::VERSION = '0.002000';
3 26     26   332132 use strict;
  26         47  
  26         868  
4 26     26   87 use warnings;
  26         47  
  26         909  
5 26     26   91 use Future::AsyncAwait;
  26         32  
  26         123  
6 26     26   999 use Carp qw(croak);
  26         33  
  26         1122  
7 26     26   470 use PAGI::Utils ();
  26         35  
  26         34995  
8              
9              
10             sub new {
11 14     14 1 10820 my ($class, %args) = @_;
12              
13 14         23 my $app = delete $args{app};
14 14 100       37 $app = PAGI::Utils::to_app($app) if defined $app;
15              
16 14         23 my @handlers;
17             push @handlers, {
18             startup => $args{startup},
19             shutdown => $args{shutdown},
20 14 100 100     56 } if $args{startup} || $args{shutdown};
21              
22 14         89 return bless {
23             app => $app,
24             _handlers => \@handlers,
25             _state => undef,
26             }, $class;
27             }
28              
29 0     0 1 0 sub state { shift->{_state} }
30              
31             sub on_startup {
32 2     2 0 10 my ($self, $cb) = @_;
33 2         5 return $self->register(startup => $cb);
34             }
35              
36             sub on_shutdown {
37 3     3 0 14 my ($self, $cb) = @_;
38 3         8 return $self->register(shutdown => $cb);
39             }
40              
41             sub register {
42 9     9 0 36 my ($self, %args) = @_;
43 9 50 66     24 return $self unless $args{startup} || $args{shutdown};
44 9         27 push @{$self->{_handlers}}, {
45             startup => $args{startup},
46             shutdown => $args{shutdown},
47 9         8 };
48 9         17 return $self;
49             }
50              
51             sub for_scope {
52 5     5 0 8 my ($class, $scope) = @_;
53 5 50 33     16 croak "scope is required" unless $scope && ref($scope) eq 'HASH';
54 5   33     17 return $scope->{'pagi.lifespan.manager'} //= $class->new;
55             }
56              
57             sub wrap {
58 3     3 1 12073 my ($class, $app, %args) = @_;
59              
60 3         11 my $self = $class->new(app => $app, %args);
61 3         9 return $self->to_app;
62             }
63              
64             sub to_app {
65 8     8 1 18 my ($self) = @_;
66              
67 8         14 my $app = $self->{app};
68 8 50       14 croak "PAGI::Lifespan->to_app requires an app" unless $app;
69              
70 13     13   472 my $wrapper = async sub {
71 13         22 my ($scope, $receive, $send) = @_;
72              
73 13   50     32 my $type = $scope->{type} // '';
74              
75 13 100       36 if ($type eq 'lifespan') {
76 6   33     22 $scope->{'pagi.lifespan.manager'} //= $self;
77 6   100     16 $scope->{state} //= {};
78 6         14 await $app->($scope, $receive, $send);
79 6         298 return await $self->handle($scope, $receive, $send);
80             }
81              
82 7         51 my $inner_scope = { %$scope };
83 7   100     50 $inner_scope->{state} //= ($self->{_state} // {});
      66        
84 7         11 $self->{_state} = $inner_scope->{state};
85              
86 7         19 await $app->($inner_scope, $receive, $send);
87 8         37 };
88              
89 8         17 return $wrapper;
90             }
91              
92 12     12 0 21 async sub handle {
93 12         22 my ($self, $scope, $receive, $send) = @_;
94 12 50 50     47 return 0 unless $scope && ($scope->{type} // '') eq 'lifespan';
      33        
95 12 50       26 return 0 if $scope->{'pagi.lifespan.handled'};
96 12         18 $scope->{'pagi.lifespan.handled'} = 1;
97              
98 12         11 my @handlers;
99 12 100       39 if (my $extra = $scope->{'pagi.lifespan.handlers'}) {
100 3         18 push @handlers, @$extra;
101             }
102 12   50     15 push @handlers, @{$self->{_handlers} // []};
  12         27  
103              
104 12   100     25 my $state = $scope->{state} //= {};
105 12         17 $self->{_state} = $state;
106              
107 12         15 while (1) {
108 22         237 my $msg = await $receive->();
109 22   50     1024 my $type = $msg->{type} // '';
110              
111 22 100       59 if ($type eq 'lifespan.startup') {
    50          
112 12         21 for my $handler (@handlers) {
113 20 100       36 next unless $handler->{startup};
114 15         13 eval { await $handler->{startup}->($state) };
  15         47  
115 15 100       488 if ($@) {
116 2         8 await $send->({
117             type => 'lifespan.startup.failed',
118             message => "$@",
119             });
120 2         53 return;
121             }
122             }
123 10         21 await $send->({ type => 'lifespan.startup.complete' });
124             }
125             elsif ($type eq 'lifespan.shutdown') {
126             # Run every shutdown handler (best-effort cleanup: one failing
127             # handler must not prevent the others from releasing resources),
128             # collecting any errors so they can be reported rather than swallowed.
129 10         9 my @errors;
130 10         20 for my $handler (reverse @handlers) {
131 18 100       37 next unless $handler->{shutdown};
132 14         17 eval { await $handler->{shutdown}->($state) };
  14         37  
133 14 100       443 push @errors, "$@" if $@;
134             }
135 10 100       16 if (@errors) {
136 2         8 await $send->({
137             type => 'lifespan.shutdown.failed',
138             message => join("\n", @errors),
139             });
140             }
141             else {
142 8         21 await $send->({ type => 'lifespan.shutdown.complete' });
143             }
144 10         244 return 1;
145             }
146             }
147             }
148              
149             1;
150              
151             __END__