File Coverage

blib/lib/Kelp/Module/WebSocket/AnyEvent.pm
Criterion Covered Total %
statement 25 54 46.3
branch 1 10 10.0
condition 1 3 33.3
subroutine 8 15 53.3
pod 4 5 80.0
total 39 87 44.8


line stmt bran cond sub pod time code
1             package Kelp::Module::WebSocket::AnyEvent;
2              
3             our $VERSION = '1.03';
4              
5 1     1   6956 use Kelp::Base qw(Kelp::Module::Symbiosis::Base);
  1         3  
  1         7  
6 1     1   2239 use Plack::App::WebSocket;
  1         16643  
  1         39  
7 1     1   603 use Kelp::Module::WebSocket::AnyEvent::Connection;
  1         6  
  1         7  
8 1     1   40 use Carp qw(croak carp cluck);
  1         3  
  1         58  
9 1     1   7 use Try::Tiny;
  1         4  
  1         938  
10              
11             attr "-serializer";
12             attr "-connections" => sub { {} };
13              
14             attr "on_open" => sub {
15             sub { }
16             };
17             attr "on_close" => sub {
18             sub { }
19             };
20             attr "on_message" => sub {
21             sub { }
22             };
23             attr "on_error";
24              
25             # This function is here to work around Twiggy bug that is silencing errors
26             # Warn them instead, so they can be logged and spotted
27             sub _trap(&)
28             {
29 0     0   0 my ($block) = @_;
30             try {
31 0     0   0 $block->();
32             }
33             catch {
34 0     0   0 cluck $_;
35 0         0 die $_;
36 0         0 };
37             }
38              
39 1     1 1 49 sub name { 'websocket' }
40              
41             sub psgi
42             {
43 0     0 1 0 my ($self) = @_;
44              
45 0         0 my $conn_max_id = 0;
46             my $websocket = Plack::App::WebSocket->new(
47              
48             # on_error - optional
49 0     0   0 (defined $self->on_error ? (on_error => sub { $self->on_error->(@_) }) : ()),
50              
51             # on_establish - mandatory
52             on_establish => sub {
53 0     0   0 my ($orig_conn, $env) = @_;
54              
55 0         0 my $conn = Kelp::Module::WebSocket::AnyEvent::Connection->new(
56             connection => $orig_conn,
57             id => ++$conn_max_id,
58             manager => $self
59             );
60 0         0 _trap { $self->on_open->($conn, $env) };
  0         0  
61              
62             $conn->connection->on(
63             message => sub {
64 0         0 my ($orig_conn, $message) = @_;
65 0 0       0 if (my $s = $self->get_serializer) {
66 0         0 $message = $s->decode($message);
67             }
68 0         0 _trap { $self->on_message->($conn, $message) };
  0         0  
69             },
70             finish => sub {
71 0         0 _trap { $self->on_close->($conn) };
  0         0  
72 0         0 $conn->close;
73 0         0 undef $orig_conn;
74             },
75 0         0 );
76             }
77 0 0       0 );
78              
79 0         0 return $websocket->to_app;
80             }
81              
82             sub add
83             {
84 3     3 1 108 my ($self, $type, $sub) = @_;
85              
86 3         8 $type = "on_$type";
87 3         17 my $setter = $self->can($type);
88 3 50       8 croak "unknown websocket event `$type`"
89             unless defined $setter;
90              
91 3         8 return $setter->($self, $sub);
92             }
93              
94             sub get_serializer
95             {
96 0     0 0 0 my ($self) = @_;
97 0 0       0 return undef unless defined $self->serializer;
98              
99 0         0 my $real_serializer_method = $self->app->can($self->serializer);
100 0 0       0 croak "Kelp doesn't have $self->serializer serializer"
101             unless defined $real_serializer_method;
102              
103 0         0 return $real_serializer_method->($self->app);
104             }
105              
106             sub build
107             {
108 1     1 1 127 my ($self, %args) = @_;
109 1         10 $self->SUPER::build(%args);
110 1   33     22 $self->{serializer} = $args{serializer} // $self->serializer;
111              
112 1         25 $self->register(websocket => $self);
113             }
114              
115             1;
116             __END__
117              
118             =head1 NAME
119              
120             Kelp::Module::WebSocket::AnyEvent - AnyEvent websocket server integration with Kelp
121              
122             =head1 SYNOPSIS
123              
124             # in config
125              
126             modules => [qw(Symbiosis WebSocket::AnyEvent)],
127             modules_init => {
128             "WebSocket::AnyEvent" => {
129             mount => '/ws',
130             serializer => "json",
131             },
132             },
133              
134              
135             # in application's build method
136              
137             my $ws = $self->websocket;
138             $ws->add(message => sub {
139             my ($conn, $msg) = @_;
140             $conn->send({received => $msg});
141             });
142              
143             # can also be mounted like this, if not specified in config
144             $self->symbiosis->mount("/ws" => $ws); # by module object
145             $self->symbiosis->mount("/ws" => 'websocket'); # by name
146              
147              
148             # in psgi script
149              
150             $app = MyApp->new;
151             $app->run_all;
152              
153              
154             =head1 DESCRIPTION
155              
156             This is a module that integrates a websocket instance into Kelp using L<Kelp::Module::Symbiosis>. To run it, a non-blocking Plack server based on AnyEvent is required, like L<Twiggy>. All this module does is wrap L<Plack::App::WebSocket> instance in Kelp's module, introduce a method to get this instance in Kelp and integrate it into running alongside Kelp using Symbiosis. An instance of this class will be available in Kelp under the I<websocket> method.
157              
158             =head1 METHODS
159              
160             =head2 name
161              
162             sig: name($self)
163              
164             Reimplemented from L<Kelp::Module::Symbiosis::Base>. Returns a name of a module that can be used in C<< $symbiosis->loaded >> hash or when mounting by name. The return value is constant string I<'websocket'>.
165              
166             Requires Symbiosis version I<1.10> for name mounting to function.
167              
168             =head2 connections
169              
170             sig: connections($self)
171              
172             Returns a hashref containing all available L<Kelp::Module::WebSocket::AnyEvent::Connection> instances (open connections) keyed by their unique id. An id is autoincremented from 1 and guaranteed not to change and not to be replaced by a different connection unless the server restarts.
173              
174             =head2 middleware
175              
176             sig: middleware($self)
177              
178             Returns an arrayref of all middlewares in format: C<[ middleware_class, [ middleware_config ] ]>.
179              
180             =head2 psgi
181              
182             sig: psgi($self)
183              
184             Returns a ran instance of L<Plack::App::WebSocket>.
185              
186             =head2 run
187              
188             sig: run($self)
189              
190             Same as psgi, but wraps the instance in all wanted middlewares.
191              
192             =head2 add
193              
194             sig: add($self, $event, $handler)
195              
196             Registers a $handler (coderef) for websocket $event (string). Handler will be passed an instance of L<Kelp::Module::WebSocket::AnyEvent::Connection> and an incoming message. $event can be either one of: I<open close message error>. You can only specify one handler for each event type.
197              
198             =head1 SEE ALSO
199              
200             =over 2
201              
202             =item * L<Dancer2::Plugin::WebSocket>, same integration for Dancer2 framework this module was inspired by
203              
204             =item * L<Kelp>, the framework
205              
206             =item * L<Twiggy>, a server capable of running this websocket
207              
208             =back
209              
210             =head1 AUTHOR
211              
212             Bartosz Jarzyna, E<lt>brtastic.dev@gmail.comE<gt>
213              
214             =head1 COPYRIGHT AND LICENSE
215              
216             Copyright (C) 2020 by Bartosz Jarzyna
217              
218             This library is free software; you can redistribute it and/or modify
219             it under the same terms as Perl itself, either Perl version 5.10.0 or,
220             at your option, any later version of Perl 5 you may have available.
221              
222              
223             =cut