File Coverage

blib/lib/Datastar/SSE.pm
Criterion Covered Total %
statement 133 136 98.5
branch 50 58 86.2
condition 28 44 63.6
subroutine 20 22 95.4
pod 6 6 100.0
total 237 266 89.8


line stmt bran cond sub pod time code
1             package Datastar::SSE;
2 6     6   853298 use strict;
  6         16  
  6         218  
3 6     6   50 use warnings;
  6         10  
  6         343  
4 6     6   78 use 5.10.0;
  6         19  
5              
6             our $VERSION = '0.26';
7              
8 6     6   3905 use JSON ();
  6         76333  
  6         222  
9 6     6   3048 use HTTP::ServerEvent;
  6         3071  
  6         218  
10 6     6   38 use Scalar::Util qw/reftype/;
  6         13  
  6         322  
11 6     6   29 use Exporter qw/import unimport/;
  6         11  
  6         216  
12              
13             # use Datastar::SSE::Types qw/is_ScalarRef is_ArrayRef is_Int/;
14 6     6   2616 use Datastar::SSE::Types qw/:is/;
  6         19  
  6         1997  
15              
16             my @execute_script_attributes = (
17             { type => 'module' },
18             );
19              
20             =pod
21              
22             =encoding utf-8
23              
24             =head1 NAME
25              
26             Datastar::SSE - Module for creating Datastar Server Events
27              
28             =head1 DESCRIPTION
29              
30             An implementation of the L<< Datastar|https://data-star.dev/ >> Server Sent Event SDK in Perl
31              
32             =head1 SYNOPSIS
33              
34             use Datastar::SSE qw/:fragment_merge_modes/;
35            
36             my @events;
37             push @events, Datastar::SSE->merge_fragments( $html_fragment, +{
38             selector => '#name-selector',
39             merge_mode => FRAGMENT_MERGEMODE_OUTER,
40             });
41             # $event is a multiline string which should be sent as part of
42             # the http response body. Multiple event strings can be sent in the same response.
43            
44             for my $evt (@events) {
45             $cgi->print( $evt ); # CGI
46             $psgi_writer->write( $evt ); # PSGI delayed response "writer"
47             $c->write( $evt ); # Mojolicious controller
48             }
49              
50             =cut
51              
52             my @datastar_events;
53             my @merge_mode;
54             my %DATASTAR_EVENTS;
55             my %MERGEMODES;
56             BEGIN {
57 6     6   29 my @datastar_events = qw/
58             datastar_merge_fragments
59             datastar_remove_fragments
60             datastar_merge_signals
61             datastar_remove_signals
62             datastar_execute_script
63             /;
64 6         17 @merge_mode = qw/
65             morph
66             inner
67             outer
68             prepend
69             append
70             before
71             after
72             upsertAttributes
73             /;
74 6         61 @DATASTAR_EVENTS{map uc, @datastar_events} = @datastar_events;
75 6         69 s/_/-/g for values %DATASTAR_EVENTS;
76 6         347 %MERGEMODES = +map +( "FRAGMENT_MERGEMODE_\U$_" => $_ ), @merge_mode;
77             }
78            
79 6     6   56 use constant +{ %DATASTAR_EVENTS, %MERGEMODES };
  6         33  
  6         11387  
80              
81             =head1 EXPORT TAGS
82              
83             The following tags can be specified to export constants related to the Datastar SSE
84              
85             =head2 events
86              
87             The L<< Datastar SSE|https://data-star.dev/reference/sse_events >> Event names:
88              
89             =over
90              
91             =item * DATASTAR_MERGE_FRAGMENTS
92              
93             L<< datastar-merge-fragments|https://data-star.dev/reference/sse_events#datastar-merge-fragments >>
94              
95             =item * DATASTAR_REMOVE_FRAGMENTS
96              
97             L<< datastar-remove-fragments|https://data-star.dev/reference/sse_events#datastar-remove-fragments >>
98              
99             =item * DATASTAR_MERGE_SIGNALS
100              
101             L<< datastar-merge-signals|https://data-star.dev/reference/sse_events#datastar-merge-signals >>
102              
103             =item * DATASTAR_REMOVE_SIGNALS
104              
105             L<< datastar-remove-signals|https://data-star.dev/reference/sse_events#datastar-remove-signals >>
106              
107             =item * DATASTAR_EXECUTE_SCRIPT
108              
109             L<< datastar-execute-script|https://data-star.dev/reference/sse_events#datastar-execute-script >>
110              
111             =back
112              
113             =head2 fragment_merge_modes
114              
115             The Merge Modes for the L event:
116              
117             =over
118              
119             =item * FRAGMENT_MERGEMODEMORPH
120              
121             C
122              
123             Merges the fragment using L<< Idiomorph|https://github.com/bigskysoftware/idiomorph >>. This is the default merge strategy.
124              
125             =item * FRAGMENT_MERGEMODE_INNER
126              
127             C
128              
129             Replaces the target’s innerHTML with the fragment.
130              
131             =item * FRAGMENT_MERGEMODE_OUTER
132              
133             C
134              
135             Replaces the target’s outerHTML with the fragment.
136              
137             =item * FRAGMENT_MERGEMODE_PREPEND
138              
139             C
140              
141             Prepends the fragment to the target’s children.
142              
143             =item * FRAGMENT_MERGEMODE_APPEND
144              
145             C
146              
147             Appends the fragment to the target’s children.
148              
149             =item * FRAGMENT_MERGEMODE_BEFORE
150              
151             C
152              
153             Inserts the fragment before the target as a sibling.
154              
155             =item * FRAGMENT_MERGEMODE_AFTER
156              
157             C
158              
159             Inserts the fragment after the target as a sibling.
160              
161             =item * FRAGMENT_MERGEMODE_UPSERTATTRIBUTES
162              
163             C
164              
165             Merges attributes from the fragment into the target – useful for updating a signal.
166              
167             =back
168              
169             =cut
170              
171             our @EXPORT_OK = (keys %DATASTAR_EVENTS, keys %MERGEMODES);
172             our %EXPORT_TAGS = ( events => [keys(%DATASTAR_EVENTS)], fragment_merge_modes => [keys(%MERGEMODES)] );
173              
174             my $json; # cache
175             sub _encode_json($) {
176 29   66 29   339 ($json ||= JSON->new->allow_blessed->convert_blessed)->encode( @_ );
177             }
178              
179             sub _decode_json($) {
180             # uncoverable subroutine
181 0   0 0   0 ($json ||= JSON->new->allow_blessed->convert_blessed)->decode( @_ ); # uncoverable statement
182             }
183              
184             =head1 METHODS
185              
186             =head2 headers
187              
188             ->headers();
189              
190             Returns an Array Ref of the recommended headers to sent for Datastar SSE responses.
191              
192             Content-Type: text/event-stream
193             Cache-Control: no-cache
194             Connection: keep-alive
195             Keep-Alive: timeout=300, max=100000
196              
197             =cut
198              
199             my $headers;
200             sub headers {
201 0   0 0 1 0 $headers ||= +[
202             'Content-Type', 'text/event-stream',
203             'Cache-Control', 'no-cache',
204             'Connection', 'keep-alive',
205             'Keep-Alive', 'timeout=300, max=100000'
206             ]
207             }
208              
209             =head1 EVENTS
210              
211             Each Datastar SSE event is implements as a class method on L. Each method accepts, but does not require,
212             an options hashref as the last parameter, the options are documented per event, additionally all options from
213             L are supported.
214              
215             =over
216              
217             =item * id
218              
219             The event id. If you send this, a client will send the "Last-Event-Id" header when reconnecting, allowing you to send the events missed
220             while offline. Newlines or null characters in the event id are treated as a fatal error.
221              
222             =item * retry
223              
224             the amount of miliseconds to wait before reconnecting if the connection is lost. Newlines or null characters in the retry interval are
225             treated as a fatal error.
226              
227             =back
228              
229             =head2 merge_fragments
230              
231             ->merge_fragments( $html_fragment, $options_hashref );
232             ->merge_fragments( $html_fragment_arrayref, $options_hashref );
233              
234             L<< datastar-merge-fragments|https://data-star.dev/reference/sse_events#datastar-merge-fragments >>
235              
236             Merges one or more fragments into the DOM. By default, Datastar merges fragments using L<< Idiomorph|https://github.com/bigskysoftware/idiomorph >>,
237             which matches top level elements based on their ID.
238              
239             =head3 OPTIONS
240              
241             =over
242              
243             =item * selector
244              
245             B
246              
247             Selects the target element of the merge process using a CSS selector.
248              
249             =item * use_view_transition
250              
251             B
252              
253             B: 0
254              
255             B: C
256              
257             Whether to use view transitions when merging into the DOM.
258              
259             =item * merge_mode
260              
261             B
262              
263             B: FRAGMENT_MERGEMODE_MORPH
264              
265             B: C
266              
267             The mode to use when merging into the DOM.
268              
269             See L<< merge modes|/merge_modes >>
270              
271             =back
272              
273             =cut
274              
275             sub merge_fragments {
276 12     12 1 256920 my $class = shift;
277 12         33 my ($fragment, $options) = @_;
278 12         25 my $event = DATASTAR_MERGE_FRAGMENTS;
279 12         23 my @data;
280 12 100       48 return "" unless $fragment;
281 11   50     31 $fragment ||= [];
282 11 100       34 if (!is_ArrayRef($fragment)) {
283 9         27 $fragment = [$fragment];
284             }
285              
286 11 100       38 if (my $selector = delete $options->{selector}) {
287 1         4 push @data, +{ selector => $selector };
288             }
289 11 100       32 if (my $merge_mode = delete $options->{merge_mode}) {
290 3 100 100     10 if (is_Mergemode( $merge_mode ) && $merge_mode ne FRAGMENT_MERGEMODE_MORPH) {
291 1         5 push @data, +{ mergeMode => $merge_mode };
292             }
293             }
294 11 100       30 if (my $use_view_transition = delete $options->{use_view_transition}) {
295 1   50     5 $use_view_transition ||= 0;
296 1 50       4 if ($use_view_transition) {
297 1         5 push @data, +{ useViewTransition => _bool( $use_view_transition )};
298             }
299             }
300 11         28 for (@$fragment) {
301 15 100       37 my $frag = is_ScalarRef($_) ? $$_ : $_;
302 15         92 my @frags = split /\cM\cJ?|\cJ/, $frag;
303 15         31 for my $f (@frags) {
304 17         68 push @data, +{ fragments => $f }
305             }
306             }
307             $class->_datastar_event(
308 11         37 $event,
309             $options,
310             @data
311             );
312             }
313              
314             =head2 merge_signals
315              
316             ->merge_signals( $signals_hashref, $options_hashref );
317              
318             L<< datastar-merge-signals|https://data-star.dev/reference/sse_events#datastar-merge-signals >>
319              
320             Updates the signals with new values. The only_if_missing option determines whether to update the
321             signals with new values only if the key does not exist. The signals line should be a valid
322             data-signals attribute. This will get merged into the signals.
323              
324             =head3 OPTIONS
325              
326             =over
327              
328             =item * only_if_missing
329              
330             B
331              
332             B: 0
333              
334             B: C
335              
336             Only update the signals with new values if the key does not exist.
337              
338             =back
339              
340             =cut
341              
342             sub merge_signals {
343 4     4 1 221453 my $class = shift;
344 4         10 my ($signals, $options) = @_;
345 4 100       21 return "" unless $signals;
346 3   100     14 $options ||= {};
347 3         6 my $event = DATASTAR_MERGE_SIGNALS;
348 3         28 my @data;
349 3 100       10 if (exists $options->{only_if_missing}) {
350 2   100     10 my $only_if_missing = delete( $options->{only_if_missing} ) || 0;
351 2         7 push @data, +{ onlyIfMissing => _bool( $only_if_missing )};
352             }
353 3 50       8 if (ref $signals) {
354 3         8 $signals = _encode_json( $signals);
355             }
356 3         11 push @data, +{ signals => $signals };
357 3         11 $class->_datastar_event(
358             $event,
359             $options,
360             @data
361             );
362             }
363              
364             =head2 remove_fragments
365              
366             ->remove_fragments( $selector, $options_hashref )
367              
368             L<< datastar-remove-fragments|https://data-star.dev/reference/sse_events#datastar-remove-fragments >>
369              
370             Removes one or more HTML fragments that match the provided selector (B<$selector>) from the DOM.
371              
372             =cut
373              
374             sub remove_fragments {
375 2     2 1 162237 my $class = shift;
376 2         6 my ($selector, $options) = @_;
377 2 100       13 return "" unless $selector;
378 1         3 my $event = DATASTAR_REMOVE_FRAGMENTS;
379 1         7 my @data = +{
380             selector => $selector,
381             };
382 1         27 $class->_datastar_event(
383             $event,
384             $options,
385             @data
386             );
387             }
388              
389             =head2 remove_signals
390              
391             ->remove_signals( @paths, $options_hashref )
392             ->remove_signals( $paths_arrayref, $options_hashref )
393              
394             L<< datastar-remove-signals|https://data-star.dev/reference/sse_events#datastar-remove-signals >>
395              
396             Removes signals that match one or more provided paths (B<@paths>).
397              
398             =cut
399              
400             sub remove_signals {
401 3     3 1 136420 my $class = shift;
402 3         4 my @signals = @_;
403 3         4 my $options;
404 3 50 66     15 if (@signals && is_HashRef($signals[ $#signals ])) {
405 0         0 $options = pop( @signals );
406             }
407 3         4 my @data;
408 3         4 my $event = DATASTAR_REMOVE_SIGNALS;
409 3         3 my @sig;
410 3         4 for my $signal (@signals) {
411 3 100 66     9 if ($signal && !ref( $signal)) {
412 2         5 push @sig, $signal;
413             }
414 3 100       5 if (is_ArrayRef($signal)) {
415 1         3 push @sig, @$signal;
416             }
417             }
418 3 100       11 return "" unless @sig;
419 2         9 @data = map +{ paths => $_ }, @sig;
420 2         6 $class->_datastar_event(
421             $event,
422             $options,
423             @data
424             );
425             }
426              
427             =head2 execute_script
428              
429             ->execute_script( $script, $options_hashref )
430             ->execute_script( $script_arrayref, $options_hashref )
431              
432             L<< datastar-execute-script|https://data-star.dev/reference/sse_events#datastar-execute-script >>
433              
434             Executes JavaScript (B<$script> or @B<$script_arrayref>) in the browser.
435              
436             =head3 OPTIONS
437              
438             =over
439              
440             =item * auto_remove
441              
442             B
443              
444             B: 1
445              
446             B: C
447              
448             Determines whether to remove the script element after execution.
449              
450             =item * attributes
451              
452             B
453              
454             B
455              
456             B: [{ type => 'module' }]
457              
458             Each attribute adds an HTML attribute to the B<<