File Coverage

blib/lib/Mojo/AsyncAwait/Backend/Coro.pm
Criterion Covered Total %
statement 85 87 97.7
branch 24 28 85.7
condition 3 6 50.0
subroutine 18 18 100.0
pod 2 2 100.0
total 132 141 93.6


line stmt bran cond sub pod time code
1             package Mojo::AsyncAwait::Backend::Coro;
2 9     9   4587 use Mojo::Base -strict;
  9         23  
  9         54  
3              
4 9     9   1086 use Carp ();
  9         28  
  9         201  
5 9     9   4269 use Coro::State ();
  9         49441  
  9         219  
6 9     9   60 use Mojo::Util;
  9         38  
  9         300  
7 9     9   464 use Mojo::Promise;
  9         110273  
  9         99  
8 9     9   255 use Scalar::Util ();
  9         18  
  9         129  
9 9     9   43 use Sub::Util ();
  9         33  
  9         160  
10              
11 9     9   41 use Exporter 'import';
  9         17  
  9         4625  
12              
13             our @EXPORT = (qw/async await/);
14              
15             my $main = Coro::State->new;
16             $main->{desc} = 'Mojo::AsyncAwait::Backend::Coro/$main';
17              
18             # LIFO stack of coroutines waiting to come back to
19             # always has $main as the bottom of the stack
20             my @stack = ($main);
21              
22             # Coroutines that are ostensible done but need someone to kill them
23             my @clean;
24              
25             # _push adds a coroutine to the stack and enters it
26             # when control returns to the original pusher, it will clean up
27             # any coroutines that are waiting to be cleaned up
28              
29             sub _push {
30 23     23   76 push @stack, @_;
31 23         1244 $stack[-2]->transfer($stack[-1]);
32 23         351 $_->cancel for @clean;
33 23         80 @clean = ();
34             }
35              
36             # _pop pops the current coroutine off the stack. If given a callback, it calls
37             # a callback on it, otherwise, schedules it for cleanup. It then transfers to
38             # the next one on the stack. Note that it can't pop-and-return (which would
39             # make more sense) because any action on it must happen before control is
40             # transfered away from it
41              
42             sub _pop (;&) {
43 23 50   23   122 Carp::croak "Cannot leave the main thread"
44             if $stack[-1] == $main;
45 23         73 my ($cb) = @_;
46 23         53 my $current = pop @stack;
47 23 100       83 if ($cb) { $cb->($current) }
  11         35  
48 12         29 else { push @clean, $current }
49 23         1141 $current->transfer($stack[-1]);
50             }
51              
52             sub async {
53 12     12 1 16789 my $body = pop;
54 12         52 my $opts = _parse_opts(@_);
55 12         65 my @caller = caller;
56              
57 12         44 my $subname = "$caller[0]::__ASYNCSUB__";
58 12         31 my $bodyname = "$caller[0]::__ASYNCBODY__";
59 12 100       54 if (defined(my $name = $opts->{-name})) {
60 3 100       18 $subname = $opts->{-install} ? "$caller[0]::$name" : "$subname($name)";
61 3         13 $bodyname .= "($name)";
62             }
63 12         46 my $desc = "declared at $caller[1] line $caller[2]";
64              
65 12 50       247 Sub::Util::set_subname($bodyname => $body)
66             if Sub::Util::subname($body) =~ /::__ANON__$/;
67              
68             my $wrapped = sub {
69 12     12   43621 my @caller = caller;
        3      
70 12         134 my $promise = Mojo::Promise->new;
71             my $coro = Coro::State->new(sub {
72 12 100       50 eval {
73 9     9   2243 BEGIN { $^H{'Mojo::AsyncAwait::Backend::Coro/async'} = 1 }
74 12         53 $promise->resolve($body->(@_)); 1
  10         4258  
75             } or $promise->reject($@);
76 12         359 _pop;
77 12         770 }, @_);
78 12         122 $coro->{desc} = "$subname called at $caller[1] line $caller[2], $desc";
79 12         55 _push $coro;
80 12         84 return $promise;
81 12         77 };
82              
83 12 100       55 if ($opts->{-install}) {
84 2         16 Mojo::Util::monkey_patch $caller[0], $opts->{-name} => $wrapped;
85 2         50 return;
86             }
87              
88 10         61 Sub::Util::set_subname $subname => $wrapped;
89 10         46 return $wrapped;
90             }
91              
92             # this prototype prevents the perl tokenizer from seeing await as an
93             # indirect method
94              
95             sub await (*) {
96             {
97             # check that our caller is actually an async function
98 9     9 1 65 no warnings 'uninitialized';
  9     13   19  
  9         3926  
  13         6417  
99 13         28 my $level = 1;
100 13         179 my ($caller, $hints) = (caller($level))[3, 10];
101              
102             # being inside of an eval is ok too
103 13         96 ($caller, $hints) = (caller(++$level))[3, 10] while $caller eq '(eval)';
104              
105             Carp::croak 'await may only be called from in async function'
106 13 100       358 unless $hints->{'Mojo::AsyncAwait::Backend::Coro/async'};
107             }
108              
109 11         37 my $promise = shift;
110 11 100 66     158 $promise = Mojo::Promise->new->resolve($promise)
111             unless Scalar::Util::blessed($promise) && $promise->can('then');
112              
113 11         182 my (@retvals, $err);
114             _pop {
115 11     11   38 my $current = shift;
116             $promise->then(
117             sub {
118 11         2937781 @retvals = @_;
119 11         55 _push $current;
120             },
121             sub {
122 0         0 $err = shift;
123 0         0 _push $current;
124             }
125 11         102 );
126 11         80 };
127              
128             # "_push $current" in the above callback brings us here
129 11 50       76 Carp::croak($err) if $err;
130 11 100       95 return wantarray ? @retvals : $retvals[0];
131             }
132              
133             sub _parse_opts {
134 12 100   12   55 return {} unless @_;
135             return {
136 3 100       19 -name => shift,
137             -install => 1,
138             } if @_ == 1;
139              
140 1         4 my %opts = @_;
141             Carp::croak 'Cannot install a sub without a name'
142 1 50 33     5 if $opts{-install} && !defined $opts{-name};
143              
144 1         2 return \%opts;
145             }
146              
147             1;
148              
149             =encoding utf8
150              
151             =head1 NAME
152              
153             Mojo::AsyncAwait::Backend::Coro - An Async/Await implementation for Mojolicious using Coro
154              
155             =head1 SYNOPSIS
156              
157             use Mojolicious::Lite -signatures;
158             use Mojo::AsyncAwait;
159              
160             get '/' => async sub ($c) {
161              
162             my $mojo = await $c->ua->get_p('https://mojolicious.org');
163             my $cpan = await $c->ua->get_p('https://metacpan.org');
164              
165             $c->render(json => {
166             mojo => $mojo->result->code,
167             cpan => $cpan->result->code
168             });
169             };
170              
171             app->start;
172              
173             =head1 DESCRIPTION
174              
175             As the name suggests, L is an implementation
176             of the Async/Await pattern, using L and L. See more at
177             L.
178              
179             =head1 CAVEATS
180              
181             This implementation relies on L which does some very magical things to
182             the Perl interpreter. All caveats that apply to using L apply to
183             this module as well.
184              
185             Also note that while a L-based implementation need not rely on L
186             being called directly from an L function, it is currently prohibitied
187             because it is likely that other/future implementations will rely on that
188             behavior and thus it should not be relied upon.
189              
190             =head1 KEYWORDS
191              
192             L provides two keywords (i.e. functions), both
193             exported by default. They are re-exported by L if it is the
194             chosen implementation.
195              
196             =head2 async
197              
198             my $sub = async sub { ... };
199              
200             The async keyword wraps a subroutine as an asynchronous subroutine which is
201             able to be suspended via L. The return value(s) of the subroutine, when
202             called, will be wrapped in a L.
203              
204             The async keyword must be called with a subroutine reference, which will be the
205             body of the async subroutine.
206              
207             Note that the returned subroutine reference is not invoked for you.
208             If you want to immediately invoke it, you need to so manually.
209              
210             my $promise = async(sub{ ... })->();
211              
212             If called with a preceding name, the subroutine will be installed into the current package with that name.
213              
214             async installed_sub => sub { ... };
215             installed_sub();
216              
217             If called with key-value arguments starting with a dash, the following options are available.
218              
219             =over
220              
221             =item -install
222              
223             If set to a true value, the subroutine will be installed into the current package.
224             Default is false.
225             Setting this value to true without a C<-name> is an error.
226              
227             =item -name
228              
229             If C<-install> is false, this is a diagnostic name to be included in the subname for debugging purposes.
230             This name is seen in diagnostic information, like stack traces.
231              
232             my $named_sub = async -name => my_name => sub { ... };
233             $named_sub->();
234              
235             Otherwise this is the name that will be installed into the current package.
236              
237             =back
238              
239             Therefore, passing a bare name as is identical to setting both C<-name> and C<< -install => 1 >>.
240              
241             async -name => installed_sub, -install => 1 => sub { ... };
242             installed_sub();
243              
244             If the subroutine is installed, whether by passing a bare name or the C<-install> option, nothing is returned.
245             Otherwise the return value is the wrapped async subroutine reference.
246              
247             =head2 await
248              
249             my $tx = await Mojo::UserAgent->new->get_p('https://mojolicious.org');
250             my @results = await (async sub { ...; return @async_results })->();
251              
252             The await keyword suspends execution of an async sub until a promise is
253             fulfilled, returning the promise's results. In list context all promise results
254             are returned. For ease of use, in scalar context the first promise result is
255             returned and the remainder are discarded.
256              
257             If the value passed to await is not a promise (defined as having a C
258             method>), it will be wrapped in a Mojo::Promise for consistency. This is mostly
259             inconsequential to the user.
260              
261             Note that await can only take one promise as an argument. If you wanted to
262             await multiple promises you probably want L or less likely
263             L.
264              
265             my $results = await Mojo::Promise->all(@promises);
266              
267             =head1 SEE ALSO
268              
269             L, L, L, L
270              
271             =cut