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.002001';
3 35     35   365249 use strict;
  35         44  
  35         1210  
4 35     35   120 use warnings;
  35         46  
  35         1299  
5 35     35   140 use Future::AsyncAwait;
  35         52  
  35         212  
6 35     35   1523 use Carp qw(croak);
  35         50  
  35         1621  
7 35     35   550 use PAGI::Utils ();
  35         65  
  35         50770  
8              
9              
10             sub new {
11 14     14 1 10558 my ($class, %args) = @_;
12              
13 14         23 my $app = delete $args{app};
14 14 100       40 $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     57 } if $args{startup} || $args{shutdown};
21              
22 14         60 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 11 my ($self, $cb) = @_;
33 2         4 return $self->register(startup => $cb);
34             }
35              
36             sub on_shutdown {
37 3     3 0 16 my ($self, $cb) = @_;
38 3         6 return $self->register(shutdown => $cb);
39             }
40              
41             sub register {
42 9     9 0 16 my ($self, %args) = @_;
43 9 50 66     24 return $self unless $args{startup} || $args{shutdown};
44 9         26 push @{$self->{_handlers}}, {
45             startup => $args{startup},
46             shutdown => $args{shutdown},
47 9         10 };
48 9         26 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     34 return $scope->{'pagi.lifespan.manager'} //= $class->new;
55             }
56              
57             sub wrap {
58 3     3 1 11797 my ($class, $app, %args) = @_;
59              
60 3         10 my $self = $class->new(app => $app, %args);
61 3         20 return $self->to_app;
62             }
63              
64             sub to_app {
65 8     8 1 18 my ($self) = @_;
66              
67 8         16 my $app = $self->{app};
68 8 50       12 croak "PAGI::Lifespan->to_app requires an app" unless $app;
69              
70 13     13   480 my $wrapper = async sub {
71 13         21 my ($scope, $receive, $send) = @_;
72              
73 13   50     31 my $type = $scope->{type} // '';
74              
75 13 100       24 if ($type eq 'lifespan') {
76 6   33     23 $scope->{'pagi.lifespan.manager'} //= $self;
77 6   100     15 $scope->{state} //= {};
78 6         11 await $app->($scope, $receive, $send);
79 6         344 return await $self->handle($scope, $receive, $send);
80             }
81              
82 7         28 my $inner_scope = { %$scope };
83 7   100     28 $inner_scope->{state} //= ($self->{_state} // {});
      66        
84 7         13 $self->{_state} = $inner_scope->{state};
85              
86 7         19 await $app->($inner_scope, $receive, $send);
87 8         25 };
88              
89 8         15 return $wrapper;
90             }
91              
92 12     12 0 22 async sub handle {
93 12         17 my ($self, $scope, $receive, $send) = @_;
94 12 50 50     50 return 0 unless $scope && ($scope->{type} // '') eq 'lifespan';
      33        
95 12 50       39 return 0 if $scope->{'pagi.lifespan.handled'};
96 12         20 $scope->{'pagi.lifespan.handled'} = 1;
97              
98 12         11 my @handlers;
99 12 100       21 if (my $extra = $scope->{'pagi.lifespan.handlers'}) {
100 3         5 push @handlers, @$extra;
101             }
102 12   50     15 push @handlers, @{$self->{_handlers} // []};
  12         22  
103              
104 12   100     29 my $state = $scope->{state} //= {};
105 12         12 $self->{_state} = $state;
106              
107 12         21 while (1) {
108 22         261 my $msg = await $receive->();
109 22   50     1023 my $type = $msg->{type} // '';
110              
111 22 100       51 if ($type eq 'lifespan.startup') {
    50          
112 12         18 for my $handler (@handlers) {
113 20 100       51 next unless $handler->{startup};
114 15         14 eval { await $handler->{startup}->($state) };
  15         26  
115 15 100       437 if ($@) {
116 2         7 await $send->({
117             type => 'lifespan.startup.failed',
118             message => "$@",
119             });
120 2         53 return;
121             }
122             }
123 10         22 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         10 my @errors;
130 10         15 for my $handler (reverse @handlers) {
131 18 100       32 next unless $handler->{shutdown};
132 14         14 eval { await $handler->{shutdown}->($state) };
  14         23  
133 14 100       389 push @errors, "$@" if $@;
134             }
135 10 100       19 if (@errors) {
136 2         7 await $send->({
137             type => 'lifespan.shutdown.failed',
138             message => join("\n", @errors),
139             });
140             }
141             else {
142 8         20 await $send->({ type => 'lifespan.shutdown.complete' });
143             }
144 10         258 return 1;
145             }
146             }
147             }
148              
149             1;
150              
151             __END__