File Coverage

examples/endpoint-router-demo/lib/MyApp/Main.pm
Criterion Covered Total %
statement 60 71 84.5
branch 1 4 25.0
condition 2 4 50.0
subroutine 13 16 81.2
pod 1 4 25.0
total 77 99 77.7


line stmt bran cond sub pod time code
1             package MyApp::Main;
2 1     1   260609 use parent 'PAGI::Endpoint::Router';
  1         1  
  1         7  
3 1     1   145 use strict;
  1         2  
  1         33  
4 1     1   6 use warnings;
  1         2  
  1         59  
5 1     1   6 use Future::AsyncAwait;
  1         3  
  1         5  
6              
7 1     1   839 use MyApp::API;
  1         5  
  1         66  
8 1     1   632 use PAGI::App::File;
  1         4  
  1         85  
9 1     1   9 use File::Spec;
  1         3  
  1         35  
10 1     1   8 use File::Basename qw(dirname);
  1         23  
  1         2087  
11              
12             sub routes {
13 1     1 1 3 my ($self, $r) = @_;
14              
15             # Home page
16 1         6 $r->get('/' => 'home');
17              
18             # API subrouter
19 1         17 $r->mount('/api' => MyApp::API->to_app);
20              
21             # WebSocket echo
22 1         6 $r->websocket('/ws/echo' => 'ws_echo');
23              
24             # SSE metrics
25 1         6 $r->sse('/events/metrics' => 'sse_metrics');
26              
27             # Static files - find the root directory dynamically
28 1         102 my $root = File::Spec->catdir(dirname(__FILE__), '..', '..', 'public');
29 1         12 $r->mount('/' => PAGI::App::File->new(root => $root)->to_app);
30             }
31              
32 1     1 0 3 async sub home {
33 1         5 my ($self, $ctx) = @_;
34              
35             # Count HTTP requests
36 1         9 $ctx->state->{metrics}{requests}++;
37              
38             # Access config via $ctx->state (populated by Lifespan startup)
39 1         5 my $config = $ctx->state->{config};
40              
41 1         35 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         7 await $ctx->response->html($html);
271             }
272              
273 1     1 0 2 async sub ws_echo {
274 1         2 my ($self, $ctx) = @_;
275 1         6 my $ws = $ctx->websocket;
276              
277 1         4 await $ws->accept;
278 1         25 await $ws->keepalive(25);
279              
280             # Access metrics via $ctx->state (populated by Lifespan startup)
281 1         30 my $metrics = $ctx->state->{metrics};
282 1         3 $metrics->{requests}++; # Count the WebSocket upgrade request
283 1         2 $metrics->{ws_active}++;
284 1   50     6 $metrics->{ws_messages} //= 0;
285              
286             $ws->on_close(sub {
287 1     1   3 $metrics->{ws_active}--;
288 1         7 });
289              
290 1         4 await $ws->send_json({ type => 'connected' });
291              
292 0     0   0 await $ws->each_json(async sub {
293 0         0 my ($data) = @_;
294 0         0 $metrics->{ws_messages}++; # Count each message received
295 0         0 await $ws->send_json({ type => 'echo', data => $data });
296 1         41 });
297             }
298              
299 1     1 0 2 async sub sse_metrics {
300 1         3 my ($self, $ctx) = @_;
301 1         4 my $sse = $ctx->sse;
302              
303             # Access metrics via $ctx->state (populated by Lifespan startup)
304 1         11 my $metrics = $ctx->state->{metrics};
305              
306             # Track sequence number for reconnection detection
307             # In production, you'd store this per-client or use timestamps
308 1   50     5 $metrics->{sse_seq} //= 0;
309              
310             # Check if this is a reconnection (browser sends Last-Event-ID header)
311 1 50       4 if (my $last_id = $sse->last_event_id) {
312 0         0 my $missed = $metrics->{sse_seq} - $last_id;
313             await $sse->send_event(
314             event => 'reconnected',
315             data => {
316             last_seen_id => $last_id,
317             current_id => $metrics->{sse_seq},
318             missed => $missed,
319             message => "Welcome back! You missed $missed updates.",
320             },
321             id => $metrics->{sse_seq},
322 0         0 retry => 3000, # Reconnect after 3 seconds if disconnected
323             );
324             } else {
325             await $sse->send_event(
326             event => 'connected',
327             data => { status => 'ok', message => 'Fresh connection' },
328             id => $metrics->{sse_seq},
329 1         6 retry => 3000,
330             );
331             }
332              
333             # Enable keepalive to prevent proxy timeouts
334 1         22 await $sse->keepalive(15);
335              
336             # Log disconnect reason - useful for debugging connection issues
337             $sse->on_close(sub {
338 0     0   0 my ($sse, $reason) = @_;
339             # In production, you might track this in metrics or logs
340 0 0       0 warn "SSE client disconnected: $reason\n"
341             unless $reason eq 'client_closed'; # Only log unexpected disconnects
342 1         25 });
343              
344             # Send metrics every 2 seconds using loop-agnostic every()
345             # Requires Future::IO to be installed
346 0     0     await $sse->every(2, async sub {
347 0           $metrics->{sse_seq}++;
348             await $sse->send_event(
349             event => 'metrics',
350             data => $metrics,
351             id => $metrics->{sse_seq},
352 0           );
353 1         6 });
354             }
355              
356             1;