File Coverage

blib/lib/Conduit.pm
Criterion Covered Total %
statement 39 50 78.0
branch 3 4 75.0
condition n/a
subroutine 10 12 83.3
pod 2 3 66.6
total 54 69 78.2


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2026 -- leonerd@leonerd.org.uk
5              
6 6     6   2389988 use v5.36;
  6         28  
7              
8 6     6   3968 use Future::AsyncAwait;
  6         56764  
  6         47  
9 6     6   5184 use Object::Pad 0.800;
  6         42650  
  6         606  
10              
11             package Conduit 0.03;
12             class Conduit
13             :strict(params);
14              
15             =head1 NAME
16              
17             C - serve HTTP with L
18              
19             =head1 SYNOPSIS
20              
21             =for highlighter language=perl
22              
23             use Conduit;
24             use Future::AsyncAwait;
25              
26             my $server = Conduit->new(
27             port => 8080,
28             psgi_app => sub ( $env ) {
29             return [
30             200,
31             [ "Content-Type" => "text/plain" ],
32             [ "Hello, world!" ]
33             ];
34             },
35             );
36              
37             await $server->run;
38              
39             =head1 DESCRIPTION
40              
41             This module allows a program to respond asynchronously to HTTP requests as
42             part of a program based on L. It currently supports either a
43             simple L-based responder or a PSGI application, but the
44             intention is to allow PAGI and possibly other interface shapes in a later
45             version.
46              
47             This is currently B, serving also as a testbed for how to design
48             larger systems using C, and hopefully soon as a way to test out
49             the PAGI design.
50              
51             This module reports basic metrics about received requests and sent responses
52             via L, as described by L.
53              
54             =cut
55              
56 6     6   3319 use Carp;
  6         18  
  6         506  
57              
58 6     6   833 use Future::IO 0.17;
  6         50714  
  6         377  
59 6     6   4111 use Future::Selector;
  6         42253  
  6         518  
60 6     6   4684 use IO::Socket::IP;
  6         293875  
  6         45  
61              
62 6     6   9028 use Conduit::Client;
  6         29  
  6         12397  
63              
64             =head1 PARAMETERS
65              
66             =head2 port
67              
68             port => $int
69              
70             TCP port number to listen on for HTTP requests.
71              
72             Either this or the C parameter must be provided; though the latter
73             is intended for internal and unit-test purposes and will not be otherwise
74             documented.
75              
76             =cut
77              
78             field $listensock :param = undef;
79             ADJUST :params (
80             :$port = undef,
81             ) {
82             defined( $port ) or defined( $listensock ) or
83             croak "Require either 'port' or 'listensock'";
84              
85             $listensock //= IO::Socket::IP->new(
86             LocalPort => $port,
87             Listen => 5,
88             ReuseAddr => 1,
89             );
90             }
91              
92 0     0 1 0 method port () { return $listensock->sockport; }
  0         0  
  0         0  
  0         0  
93              
94             =head2 responder
95              
96             responder => $app
97              
98             $response = await $app->( $request );
99              
100             A code reference to the responder used for handling requests.
101              
102             It will be passed an L instance containing the incoming request
103             and is expected to yield an L instance via a future. As a
104             small convenience, the server will fill in the C and
105             C fields of the response if they are not provided by the
106             responder.
107              
108             =head2 psgi_app
109              
110             psgi_app => $app
111              
112             A code reference to the L application used for handling requests.
113              
114             Currently, exactly one of C or C must be provided, but
115             the intention is soon to allow other forms of responders, such as L as
116             alternatives.
117              
118             =cut
119              
120             field $responder :param = undef;
121             field $psgi_app :param = undef;
122              
123             ADJUST {
124             defined $responder or defined $psgi_app or
125             croak "Require either 'responder' or 'psgi_app'";
126              
127             defined( $responder ) + defined( $psgi_app ) == 1 or
128             croak "Require exactly one of 'responder' or 'psgi_app'";
129             }
130              
131             =head1 METHODS
132              
133             =cut
134              
135             field $selector = Future::Selector->new;
136              
137 8     8 0 20 async method listen ()
  8         53  
  8         16  
138 8         18 {
139 8         75 while( my $clientsock = await Future::IO->accept( $listensock ) ) {
140 8         3599 my $client;
141 8 100       101 if( $responder ) {
    50          
142 3         74 $client = Conduit::Client::_ForHTTP->new(
143             responder => $responder,
144             server => $self,
145             socket => $clientsock,
146             );
147             }
148             elsif( $psgi_app ) {
149 5         143 $client = Conduit::Client::_ForPSGI->new(
150             psgi_app => $psgi_app,
151             server => $self,
152             socket => $clientsock,
153             );
154             }
155             else {
156 0         0 die "TODO: other app shapes?";
157             }
158              
159 0         0 $selector->add(
160             data => $client,
161             f => $client->run
162 0     0   0 ->else( sub ( $err, @ ) {
  0         0  
163 0         0 chomp $err;
164 0         0 warn "Conduit client connection failed: $err\n";
165 0         0 Future->done; # don't make the server fail
166 8         57 }),
167             );
168             }
169             }
170              
171             =head2 run
172              
173             $run_f = $conduit->run;
174              
175             Starts operation of the server, allowing it to accept new connections, serve
176             requests, and run the application.
177              
178             Returns a L instance that in normal circumstances should never
179             complete; it will remain pending indefinitely. The toplevel program can either
180             C this if it has nothing else to do, or add that to a collection such
181             as with L.
182              
183             =cut
184              
185 8     8 1 10377 async method run ()
  8         41  
  8         15  
186 8         21 {
187 8         46 $selector->add( data => "accept", f => $self->listen );
188 8         47663 await $selector->run;
189             }
190              
191             =head1 TODO
192              
193             Honestly, quite a lot. Almost everything in fact. ;)
194              
195             =over 4
196              
197             =item *
198              
199             L support; likely in preference to any more PSGI.
200              
201             =item *
202              
203             Maybe support streaming PSGI responses, though it would still be preferrable
204             to do this with PAGI first.
205              
206             =item *
207              
208             Investigate split IPv4+IPv6 serving, whether it needs two socket or one will
209             suffice. This may be OS-dependent.
210              
211             =item *
212              
213             HTTPS, perhaps via L or maybe something newer?
214              
215             =item *
216              
217             Look into what's required to support some sort of websocket thing in addition
218             to plain HTTP.
219              
220             =back
221              
222             =cut
223              
224             =head1 AUTHOR
225              
226             Paul Evans
227              
228             =cut
229              
230             0x55AA;