File Coverage

examples/endpoint-router-demo/lib/MyApp/Main.pm
Criterion Covered Total %
statement 58 69 84.0
branch 1 4 25.0
condition 2 4 50.0
subroutine 13 16 81.2
pod 1 4 25.0
total 75 97 77.3


line stmt bran cond sub pod time code
1             package MyApp::Main;
2 1     1   217370 use parent 'PAGI::Endpoint::Router';
  1         1  
  1         20  
3 1     1   97 use strict;
  1         1  
  1         79  
4 1     1   6 use warnings;
  1         1  
  1         46  
5 1     1   4 use Future::AsyncAwait;
  1         2  
  1         5  
6              
7 1     1   706 use MyApp::API;
  1         7  
  1         69  
8 1     1   487 use PAGI::App::File;
  1         3  
  1         59  
9 1     1   7 use File::Spec;
  1         1  
  1         35  
10 1     1   57 use File::Basename qw(dirname);
  1         3  
  1         1412  
11              
12             sub routes {
13 1     1 1 2 my ($self, $r) = @_;
14              
15             # Home page
16 1         102 $r->get('/' => 'home');
17              
18             # API subrouter
19 1         16 $r->mount('/api' => MyApp::API->to_app);
20              
21             # WebSocket echo
22 1         4 $r->websocket('/ws/echo' => 'ws_echo');
23              
24             # SSE metrics
25 1         4 $r->sse('/events/metrics' => 'sse_metrics');
26              
27             # Static files - find the root directory dynamically
28 1         82 my $root = File::Spec->catdir(dirname(__FILE__), '..', '..', 'public');
29 1         8 $r->mount('/' => PAGI::App::File->new(root => $root)->to_app);
30             }
31              
32 1     1 0 3 async sub home {
33 1         2 my ($self, $req, $res) = @_;
34              
35             # Count HTTP requests
36 1         4 $req->state->{metrics}{requests}++;
37              
38             # Access config via $req->state (populated by Lifespan startup)
39 1         3 my $config = $req->state->{config};
40              
41 1         18 my $html = <<"HTML";
42            
43            
44            
45             $config->{app_name}
46            
62            
63            
64            

$config->{app_name}

65            

Version: $config->{version} | Demonstrates PAGI::Endpoint::Router with HTTP, WebSocket, and SSE

66              
67            
68            

REST API

69            

70             /api/info - App information |
71             /api/users - List users |
72             /api/users/1 - Get user by ID
73            

74            

Users

75            
76            

Create User

77            

78            
79            
80            
81            
82            

83            
84              
85            
86            

WebSocket Echo Disconnected

87            

88            
89            
90            
91            

92            
93            
94              
95            
96            

SSE Metrics Stream Disconnected

97            

98            
99            
100             Try disconnecting and reconnecting to see missed event count!
101            

102            
103            
104              
105            
266            
267            
268             HTML
269              
270 1         4 await $res->html($html);
271             }
272              
273 1     1 0 2 async sub ws_echo {
274 1         2 my ($self, $ws) = @_;
275              
276 1         5 await $ws->accept;
277 1         27 await $ws->keepalive(25);
278              
279             # Access metrics via $ws->state (populated by Lifespan startup)
280 1         22 my $metrics = $ws->state->{metrics};
281 1         3 $metrics->{requests}++; # Count the WebSocket upgrade request
282 1         1 $metrics->{ws_active}++;
283 1   50     7 $metrics->{ws_messages} //= 0;
284              
285             $ws->on_close(sub {
286 1     1   3 $metrics->{ws_active}--;
287 1         9 });
288              
289 1         4 await $ws->send_json({ type => 'connected' });
290              
291 0     0   0 await $ws->each_json(async sub {
292 0         0 my ($data) = @_;
293 0         0 $metrics->{ws_messages}++; # Count each message received
294 0         0 await $ws->send_json({ type => 'echo', data => $data });
295 1         47 });
296             }
297              
298 1     1 0 6 async sub sse_metrics {
299 1         5 my ($self, $sse) = @_;
300              
301             # Access metrics via $sse->state (populated by Lifespan startup)
302 1         7 my $metrics = $sse->state->{metrics};
303              
304             # Track sequence number for reconnection detection
305             # In production, you'd store this per-client or use timestamps
306 1   50     11 $metrics->{sse_seq} //= 0;
307              
308             # Check if this is a reconnection (browser sends Last-Event-ID header)
309 1 50       3 if (my $last_id = $sse->last_event_id) {
310 0         0 my $missed = $metrics->{sse_seq} - $last_id;
311             await $sse->send_event(
312             event => 'reconnected',
313             data => {
314             last_seen_id => $last_id,
315             current_id => $metrics->{sse_seq},
316             missed => $missed,
317             message => "Welcome back! You missed $missed updates.",
318             },
319             id => $metrics->{sse_seq},
320 0         0 retry => 3000, # Reconnect after 3 seconds if disconnected
321             );
322             } else {
323             await $sse->send_event(
324             event => 'connected',
325             data => { status => 'ok', message => 'Fresh connection' },
326             id => $metrics->{sse_seq},
327 1         160 retry => 3000,
328             );
329             }
330              
331             # Enable keepalive to prevent proxy timeouts
332 1         42 await $sse->keepalive(15);
333              
334             # Log disconnect reason - useful for debugging connection issues
335             $sse->on_close(sub {
336 0     0   0 my ($sse, $reason) = @_;
337             # In production, you might track this in metrics or logs
338 0 0       0 warn "SSE client disconnected: $reason\n"
339             unless $reason eq 'client_closed'; # Only log unexpected disconnects
340 1         54 });
341              
342             # Send metrics every 2 seconds using loop-agnostic every()
343             # Requires Future::IO to be installed
344 0     0     await $sse->every(2, async sub {
345 0           $metrics->{sse_seq}++;
346             await $sse->send_event(
347             event => 'metrics',
348             data => $metrics,
349             id => $metrics->{sse_seq},
350 0           );
351 1         8 });
352             }
353              
354             1;