| 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 | |
||||||
| 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; |