File Coverage

blib/lib/Mojo/Promise.pm
Criterion Covered Total %
statement 152 190 80.0
branch 46 60 76.6
condition 26 42 61.9
subroutine 45 61 73.7
pod 15 27 55.5
total 284 380 74.7


line stmt bran cond sub pod time code
1             package Mojo::Promise;
2 67     67   1246 use Mojo::Base -base;
  67         150  
  67         614  
3              
4 67     67   465 use Carp qw(carp croak);
  67         130  
  67         4281  
5 67     67   26148 use Mojo::Exception;
  67         197  
  67         3515  
6 67     67   1154 use Mojo::IOLoop;
  67         135  
  67         482  
7 67     67   371 use Scalar::Util qw(blessed);
  67         204  
  67         4410  
8              
9 67   50 67   378 use constant DEBUG => $ENV{MOJO_PROMISE_DEBUG} || 0;
  67         149  
  67         300670  
10              
11             has ioloop => sub { Mojo::IOLoop->singleton }, weak => 1;
12              
13       0 0   sub AWAIT_CHAIN_CANCEL { }
14 0     0 0 0 sub AWAIT_CLONE { _await('clone', @_) }
15 0     0 0 0 sub AWAIT_DONE { _settle_await(resolve => @_) }
16 0     0 0 0 sub AWAIT_FAIL { _settle_await(reject => @_) }
17              
18             sub AWAIT_GET {
19 0     0 0 0 my $self = shift;
20 0   0     0 my @results = @{$self->{results} // []};
  0         0  
21 0 0       0 return wantarray ? @results : $results[0] if $self->{status} eq 'resolve';
    0          
22 0 0 0     0 die $results[0] if ref $results[0] || $results[0] =~ m!\n!;
23 0         0 croak $results[0];
24             }
25              
26 0     0 0 0 sub AWAIT_IS_CANCELLED {undef}
27              
28             sub AWAIT_IS_READY {
29 0     0 0 0 my $self = shift;
30 0         0 $self->{handled} = 1;
31 0   0     0 return !!$self->{results} && !@{$self->{resolve}} && !@{$self->{reject}};
32             }
33              
34 0     0 0 0 sub AWAIT_NEW_DONE { _await('resolve', @_) }
35 0     0 0 0 sub AWAIT_NEW_FAIL { _await('reject', @_) }
36              
37       0 0   sub AWAIT_ON_CANCEL { }
38              
39             sub AWAIT_ON_READY {
40 0     0 0 0 my ($self, $cb) = @_;
41 0         0 push @{$self->{resolve}}, $cb;
  0         0  
42 0         0 push @{$self->{reject}}, $cb;
  0         0  
43             }
44              
45             sub AWAIT_WAIT {
46 0     0 0 0 my $self = shift;
47 0     0   0 $self->catch(sub { })->wait;
48 0         0 return $self->AWAIT_GET;
49             }
50              
51             sub DESTROY {
52 798     798   43753 my $self = shift;
53 798 100 100     11107 return if $self->{handled} || ($self->{status} // '') ne 'reject' || !$self->{results};
      100        
      66        
54 7         16 carp "Unhandled rejected promise: @{$self->{results}}";
  7         1530  
55 7         110 warn $self->{debug}->message("-- Destroyed promise\n")->verbose(1)->to_string if DEBUG;
56             }
57              
58 11     11 1 94 sub all { _all(2, @_) }
59 2     2 1 15 sub all_settled { _all(0, @_) }
60 2     2 1 16 sub any { _all(3, @_) }
61              
62 177     177 1 1575 sub catch { shift->then(undef, shift) }
63              
64 627     627 1 1675 sub clone { $_[0]->new->ioloop($_[0]->ioloop) }
65              
66 11     11 1 105 sub finally { shift->_finally(1, @_) }
67              
68             sub map {
69 4 100   4 1 12986 my ($class, $options, $cb, @items) = (shift, ref $_[0] eq 'HASH' ? shift : {}, @_);
70              
71 4 100 66     32 return $class->all(map { $_->$cb } @items) if !$options->{concurrency} || @items <= $options->{concurrency};
  8         24  
72              
73 2         10 my @start = map { $_->$cb } splice @items, 0, $options->{concurrency};
  6         21  
74 2         10 my @wait = map { $start[0]->clone } 0 .. $#items;
  8         21  
75              
76             my $start_next = sub {
77 9 100   9   28 return () unless @items;
78 6         10 my $item = shift @items;
79 6         15 my ($start_next, $chain) = (__SUB__, shift @wait);
80 6         28 $_->$cb->then(sub { $chain->resolve(@_); $start_next->() }, sub { $chain->reject(@_); @items = () }) for $item;
  6         21  
  6         15  
  0         0  
  0         0  
81 6         17 return ();
82 2         12 };
83              
84 2     3   13 $_->then($start_next, sub { }) for @start;
85              
86 2         11 return $class->all(@start, @wait);
87             }
88              
89             sub new {
90 804     804 1 417178 my $self = shift->SUPER::new;
91 804         1253 $self->{debug} = Mojo::Exception->new->trace if DEBUG;
92 804 100   1   1739 shift->(sub { $self->resolve(@_) }, sub { $self->reject(@_) }) if @_;
  1         8  
  1         6  
93 804         3149 return $self;
94             }
95              
96 3     3 1 25 sub race { _all(1, @_) }
97              
98 99     99 1 7944 sub reject { shift->_settle('reject', @_) }
99 778     778 1 8435 sub resolve { shift->_settle('resolve', @_) }
100              
101             sub then {
102 543     543 1 1592 my ($self, $resolve, $reject) = @_;
103              
104 543         1232 my $new = $self->clone;
105 543         1158 $self->{handled} = 1;
106 543     429   766 push @{$self->{resolve}}, sub { _then_cb($new, $resolve, 'resolve', @_) };
  543         2627  
  429         1037  
107 543     88   1048 push @{$self->{reject}}, sub { _then_cb($new, $reject, 'reject', @_) };
  543         2001  
  88         250  
108              
109 543 100       1869 $self->_defer if $self->{results};
110              
111 543         1815 return $new;
112             }
113              
114 3     3 1 4951 sub timer { shift->_timer('resolve', @_) }
115 3     3 1 8539 sub timeout { shift->_timer('reject', @_) }
116              
117             sub wait {
118 46     46 1 109 my $self = shift;
119 46 50       160 return if (my $loop = $self->ioloop)->is_running;
120 46         115 my $done;
121 46     44   559 $self->_finally(0, sub { $done++; $loop->stop })->catch(sub { });
  46         110  
  46         225  
122 46         272 $loop->start until $done;
123             }
124              
125             sub _all {
126 18     18   67 my ($type, $class, @promises) = @_;
127              
128 18         76 my $all = $promises[0]->clone;
129 18         44 my $results = [];
130 18         43 my $remaining = scalar @promises;
131 18         75 for my $i (0 .. $#promises) {
132              
133             # "race"
134 62 100       299 if ($type == 1) {
    100          
    100          
135 8     5   44 $promises[$i]->then(sub { $all->resolve(@_); () }, sub { $all->reject(@_); () });
  5         17  
  5         10  
  3         13  
  3         9  
136             }
137              
138             # "all"
139             elsif ($type == 2) {
140             $promises[$i]->then(
141             sub {
142 36     36   106 $results->[$i] = [@_];
143 36 100       131 $all->resolve(@$results) if --$remaining <= 0;
144 36         80 return ();
145             },
146 4     4   18 sub { $all->reject(@_); () }
  4         9  
147 42         283 );
148             }
149              
150             # "any"
151             elsif ($type == 3) {
152             $promises[$i]->then(
153 2     2   8 sub { $all->resolve(@_); () },
  2         4  
154             sub {
155 4     4   11 $results->[$i] = [@_];
156 4 100       17 $all->reject(@$results) if --$remaining <= 0;
157 4         9 return ();
158             }
159 6         37 );
160             }
161              
162             # "all_settled"
163             else {
164             $promises[$i]->then(
165             sub {
166 5     5   26 $results->[$i] = {status => 'fulfilled', value => [@_]};
167 5 100       18 $all->resolve(@$results) if --$remaining <= 0;
168 5         11 return ();
169             },
170             sub {
171 1     1   6 $results->[$i] = {status => 'rejected', reason => [@_]};
172 1 50       4 $all->resolve(@$results) if --$remaining <= 0;
173 1         3 return ();
174             }
175 6         41 );
176             }
177             }
178              
179 18         177 return $all;
180             }
181              
182             sub _await {
183 0     0   0 my ($method, $class) = (shift, shift);
184 0         0 my $promise = $class->$method(@_);
185 0         0 $promise->{cycle} = $promise;
186 0         0 return $promise;
187             }
188              
189             sub _defer {
190 880     880   1397 my $self = shift;
191              
192 880 50       2275 return unless my $results = $self->{results};
193 880 100       2359 my $cbs = $self->{status} eq 'resolve' ? $self->{resolve} : $self->{reject};
194 880         1793 @{$self}{qw(cycle resolve reject)} = (undef, [], []);
  880         4271  
195              
196 880     880   2993 $self->ioloop->next_tick(sub { $_->(@$results) for @$cbs });
  880         3449  
197             }
198              
199             sub _finally {
200 57     57   178 my ($self, $handled, $finally) = @_;
201              
202 57         159 my $new = $self->clone;
203             my $cb = sub {
204 57     57   188 my @results = @_;
205 57         311 $new->resolve($finally->())->then(sub {@results});
  55         169  
206 57         296 };
207              
208 57         134 my $before = $self->{handled};
209 57         213 $self->catch($cb);
210 57         159 my $next = $self->then($cb);
211 57 100 100     355 delete $self->{handled} if !$before && !$handled;
212              
213 57         349 return $next;
214             }
215              
216             sub _settle {
217 877     877   2448 my ($self, $status, @results) = @_;
218              
219 877   100     3398 my $thenable = blessed $results[0] && $results[0]->can('then');
220 877 100       2285 unless (ref $self) {
221 38 50 66     178 return $results[0] if $thenable && $status eq 'resolve' && $results[0]->isa('Mojo::Promise');
      66        
222 34         100 $self = $self->new;
223             }
224              
225 873 100 100     3049 if ($thenable && $status eq 'resolve') {
    100          
226 67     39   461 $results[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () });
  61         217  
  61         150  
  6         28  
  6         15  
227             }
228             elsif (!$self->{results}) {
229 775         1464 @{$self}{qw(results status)} = (\@results, $status);
  775         2425  
230 775         2066 $self->_defer;
231             }
232              
233 873         6428 return $self;
234             }
235              
236             sub _settle_await {
237 0     0   0 my ($status, $self, @results) = @_;
238 0     0   0 return $results[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () })
  0         0  
  0         0  
  0         0  
239 0 0 0     0 if blessed $results[0] && $results[0]->can('then');
240 0 0       0 @{$self}{qw(results status)} = ([@results], $status) if !$self->{results};
  0         0  
241 0         0 $self->_defer;
242             }
243              
244             sub _then_cb {
245 517     517   1373 my ($new, $cb, $method, @results) = @_;
246              
247 517 100       1466 return $new->$method(@results) unless defined $cb;
248              
249 351         545 my @res;
250 351 100       591 return $new->reject($@) unless eval { @res = $cb->(@results); 1 };
  351         1051  
  349         1375  
251 349         895 return $new->resolve(@res);
252             }
253              
254             sub _timer {
255 6     6   27 my ($self, $method, $after, @results) = @_;
256 6 100       27 $self = $self->new unless ref $self;
257 6 100 100     37 $results[0] = 'Promise timeout' if $method eq 'reject' && !@results;
258 6     6   22 $self->ioloop->timer($after => sub { $self->$method(@results) });
  6         43  
259 6         65 return $self;
260             }
261              
262             1;
263              
264             =encoding utf8
265              
266             =head1 NAME
267              
268             Mojo::Promise - Promises/A+
269              
270             =head1 SYNOPSIS
271              
272             use Mojo::Promise;
273             use Mojo::UserAgent;
274              
275             # Wrap continuation-passing style APIs with promises
276             my $ua = Mojo::UserAgent->new;
277             sub get_p {
278             my $promise = Mojo::Promise->new;
279             $ua->get(@_ => sub ($ua, $tx) {
280             my $err = $tx->error;
281             if (!$err || $err->{code}) { $promise->resolve($tx) }
282             else { $promise->reject($err->{message}) }
283             });
284             return $promise;
285             }
286              
287             # Perform non-blocking operations sequentially
288             get_p('https://mojolicious.org')->then(sub ($mojo) {
289             say $mojo->res->code;
290             return get_p('https://metacpan.org');
291             })->then(sub ($cpan) {
292             say $cpan->res->code;
293             })->catch(sub ($err) {
294             warn "Something went wrong: $err";
295             })->wait;
296              
297             # Synchronize non-blocking operations (all)
298             my $mojo = get_p('https://mojolicious.org');
299             my $cpan = get_p('https://metacpan.org');
300             Mojo::Promise->all($mojo, $cpan)->then(sub ($mojo, $cpan) {
301             say $mojo->[0]->res->code;
302             say $cpan->[0]->res->code;
303             })->catch(sub ($err) {
304             warn "Something went wrong: $err";
305             })->wait;
306              
307             # Synchronize non-blocking operations (race)
308             my $mojo = get_p('https://mojolicious.org');
309             my $cpan = get_p('https://metacpan.org');
310             Mojo::Promise->race($mojo, $cpan)->then(sub ($tx) {
311             say $tx->req->url, ' won!';
312             })->catch(sub ($err) {
313             warn "Something went wrong: $err";
314             })->wait;
315              
316             =head1 DESCRIPTION
317              
318             L is a Perl-ish implementation of L and a superset of L
319             Promises|https://duckduckgo.com/?q=\mdn%20Promise>.
320              
321             =head1 STATES
322              
323             A promise is an object representing the eventual completion or failure of a non-blocking operation. It allows
324             non-blocking functions to return values, like blocking functions. But instead of immediately returning the final value,
325             the non-blocking function returns a promise to supply the value at some point in the future.
326              
327             A promise can be in one of three states:
328              
329             =over 2
330              
331             =item pending
332              
333             Initial state, neither fulfilled nor rejected.
334              
335             =item fulfilled
336              
337             Meaning that the operation completed successfully.
338              
339             =item rejected
340              
341             Meaning that the operation failed.
342              
343             =back
344              
345             A pending promise can either be fulfilled with a value or rejected with a reason. When either happens, the associated
346             handlers queued up by a promise's L method are called.
347              
348             =head1 ATTRIBUTES
349              
350             L implements the following attributes.
351              
352             =head2 ioloop
353              
354             my $loop = $promise->ioloop;
355             $promise = $promise->ioloop(Mojo::IOLoop->new);
356              
357             Event loop object to control, defaults to the global L singleton. Note that this attribute is weakened.
358              
359             =head1 METHODS
360              
361             L inherits all methods from L and implements the following new ones.
362              
363             =head2 all
364              
365             my $new = Mojo::Promise->all(@promises);
366              
367             Returns a new L object that either fulfills when all of the passed L objects have
368             fulfilled or rejects as soon as one of them rejects. If the returned promise fulfills, it is fulfilled with the values
369             from the fulfilled promises in the same order as the passed promises.
370              
371             =head2 all_settled
372              
373             my $new = Mojo::Promise->all_settled(@promises);
374              
375             Returns a new L object that fulfills when all of the passed L objects have fulfilled or
376             rejected, with hash references that describe the outcome of each promise.
377              
378             =head2 any
379              
380             my $new = Mojo::Promise->any(@promises);
381              
382             Returns a new L object that fulfills as soon as one of the passed L objects fulfills,
383             with the value from that promise.
384              
385             =head2 catch
386              
387             my $new = $promise->catch(sub {...});
388              
389             Appends a rejection handler callback to the promise, and returns a new L object resolving to the return
390             value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled.
391              
392             # Longer version
393             my $new = $promise->then(undef, sub {...});
394              
395             # Pass along the rejection reason
396             $promise->catch(sub (@reason) {
397             warn "Something went wrong: $reason[0]";
398             return @reason;
399             });
400              
401             # Change the rejection reason
402             $promise->catch(sub (@reason) { "This is bad: $reason[0]" });
403              
404             =head2 clone
405              
406             my $new = $promise->clone;
407              
408             Return a new L object cloned from this promise that is still pending.
409              
410             =head2 finally
411              
412             my $new = $promise->finally(sub {...});
413              
414             Appends a fulfillment and rejection handler to the promise, and returns a new L object resolving to the
415             original fulfillment value or rejection reason.
416              
417             # Do something on fulfillment and rejection
418             $promise->finally(sub { say "We are done!" });
419              
420             =head2 map
421              
422             my $new = Mojo::Promise->map(sub {...}, @items);
423             my $new = Mojo::Promise->map({concurrency => 3}, sub {...}, @items);
424              
425             Apply a function that returns a L to each item in a list of items while optionally limiting concurrency.
426             Returns a L that collects the results in the same manner as L. If any item's promise is rejected,
427             any remaining items which have not yet been mapped will not be.
428              
429             # Perform 3 requests at a time concurrently
430             Mojo::Promise->map({concurrency => 3}, sub { $ua->get_p($_) }, @urls)
431             ->then(sub{ say $_->[0]->res->dom->at('title')->text for @_ });
432              
433             These options are currently available:
434              
435             =over 2
436              
437             =item concurrency
438              
439             concurrency => 3
440              
441             The maximum number of items that are in progress at the same time.
442              
443             =back
444              
445             =head2 new
446              
447             my $promise = Mojo::Promise->new;
448             my $promise = Mojo::Promise->new(sub {...});
449              
450             Construct a new L object.
451              
452             # Wrap a continuation-passing style API
453             my $promise = Mojo::Promise->new(sub ($resolve, $reject) {
454             Mojo::IOLoop->timer(5 => sub {
455             if (int rand 2) { $resolve->('Lucky!') }
456             else { $reject->('Unlucky!') }
457             });
458             });
459              
460             =head2 race
461              
462             my $new = Mojo::Promise->race(@promises);
463              
464             Returns a new L object that fulfills or rejects as soon as one of the passed L objects
465             fulfills or rejects, with the value or reason from that promise.
466              
467             =head2 reject
468              
469             my $new = Mojo::Promise->reject(@reason);
470             $promise = $promise->reject(@reason);
471              
472             Build rejected L object or reject the promise with one or more rejection reasons.
473              
474             # Longer version
475             my $promise = Mojo::Promise->new->reject(@reason);
476              
477             =head2 resolve
478              
479             my $new = Mojo::Promise->resolve(@value);
480             $promise = $promise->resolve(@value);
481              
482             Build resolved L object or resolve the promise with one or more fulfillment values.
483              
484             # Longer version
485             my $promise = Mojo::Promise->new->resolve(@value);
486              
487             =head2 then
488              
489             my $new = $promise->then(sub {...});
490             my $new = $promise->then(sub {...}, sub {...});
491             my $new = $promise->then(undef, sub {...});
492              
493             Appends fulfillment and rejection handlers to the promise, and returns a new L object resolving to the
494             return value of the called handler.
495              
496             # Pass along the fulfillment value or rejection reason
497             $promise->then(
498             sub (@value) {
499             say "The result is $value[0]";
500             return @value;
501             },
502             sub (@reason) {
503             warn "Something went wrong: $reason[0]";
504             return @reason;
505             }
506             );
507              
508             # Change the fulfillment value or rejection reason
509             $promise->then(
510             sub (@value) { return "This is good: $value[0]" },
511             sub (@reason) { return "This is bad: $reason[0]" }
512             );
513              
514             =head2 timer
515              
516             my $new = Mojo::Promise->timer(5 => 'Success!');
517             $promise = $promise->timer(5 => 'Success!');
518             $promise = $promise->timer(5);
519              
520             Create a new L object with a timer or attach a timer to an existing promise. The promise will be
521             resolved after the given amount of time in seconds with or without a value.
522              
523             =head2 timeout
524              
525             my $new = Mojo::Promise->timeout(5 => 'Timeout!');
526             $promise = $promise->timeout(5 => 'Timeout!');
527             $promise = $promise->timeout(5);
528              
529             Create a new L object with a timeout or attach a timeout to an existing promise. The promise will be
530             rejected after the given amount of time in seconds with a reason, which defaults to C.
531              
532             =head2 wait
533              
534             $promise->wait;
535              
536             Start L and stop it again once the promise has been fulfilled or rejected, does nothing when L is
537             already running.
538              
539             =head1 DEBUGGING
540              
541             You can set the C environment variable to get some advanced diagnostics information printed to
542             C.
543              
544             MOJO_PROMISE_DEBUG=1
545              
546             =head1 SEE ALSO
547              
548             L, L, L.
549              
550             =cut