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   810 use Mojo::Base -base;
  67         110  
  67         568  
3              
4 67     67   326 use Carp qw(carp croak);
  67         110  
  67         3191  
5 67     67   19354 use Mojo::Exception;
  67         149  
  67         2765  
6 67     67   803 use Mojo::IOLoop;
  67         95  
  67         348  
7 67     67   322 use Scalar::Util qw(blessed);
  67         215  
  67         3206  
8              
9 67   50 67   258 use constant DEBUG => $ENV{MOJO_PROMISE_DEBUG} || 0;
  67         124  
  67         203895  
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   25951 my $self = shift;
53 798 100 100     6376 return if $self->{handled} || ($self->{status} // '') ne 'reject' || !$self->{results};
      100        
      66        
54 7         10 carp "Unhandled rejected promise: @{$self->{results}}";
  7         1077  
55 7         70 warn $self->{debug}->message("-- Destroyed promise\n")->verbose(1)->to_string if DEBUG;
56             }
57              
58 11     11 1 73 sub all { _all(2, @_) }
59 2     2 1 13 sub all_settled { _all(0, @_) }
60 2     2 1 10 sub any { _all(3, @_) }
61              
62 177     177 1 1105 sub catch { shift->then(undef, shift) }
63              
64 627     627 1 985 sub clone { $_[0]->new->ioloop($_[0]->ioloop) }
65              
66 11     11 1 57 sub finally { shift->_finally(1, @_) }
67              
68             sub map {
69 4 100   4 1 7694 my ($class, $options, $cb, @items) = (shift, ref $_[0] eq 'HASH' ? shift : {}, @_);
70              
71 4 100 66     22 return $class->all(map { $_->$cb } @items) if !$options->{concurrency} || @items <= $options->{concurrency};
  8         19  
72              
73 2         6 my @start = map { $_->$cb } splice @items, 0, $options->{concurrency};
  6         13  
74 2         7 my @wait = map { $start[0]->clone } 0 .. $#items;
  8         9  
75              
76             my $start_next = sub {
77 9 100   9   14 return () unless @items;
78 6         7 my $item = shift @items;
79 6         9 my ($start_next, $chain) = (__SUB__, shift @wait);
80 6         13 $_->$cb->then(sub { $chain->resolve(@_); $start_next->() }, sub { $chain->reject(@_); @items = () }) for $item;
  6         13  
  6         21  
  0         0  
  0         0  
81 6         9 return ();
82 2         6 };
83              
84 2     3   8 $_->then($start_next, sub { }) for @start;
85              
86 2         10 return $class->all(@start, @wait);
87             }
88              
89             sub new {
90 804     804 1 250899 my $self = shift->SUPER::new;
91 804         852 $self->{debug} = Mojo::Exception->new->trace if DEBUG;
92 804 100   1   1193 shift->(sub { $self->resolve(@_) }, sub { $self->reject(@_) }) if @_;
  1         4  
  1         5  
93 804         1517 return $self;
94             }
95              
96 3     3 1 14 sub race { _all(1, @_) }
97              
98 99     99 1 5560 sub reject { shift->_settle('reject', @_) }
99 778     778 1 5440 sub resolve { shift->_settle('resolve', @_) }
100              
101             sub then {
102 543     543 1 890 my ($self, $resolve, $reject) = @_;
103              
104 543         825 my $new = $self->clone;
105 543         792 $self->{handled} = 1;
106 543     429   556 push @{$self->{resolve}}, sub { _then_cb($new, $resolve, 'resolve', @_) };
  543         1369  
  429         733  
107 543     88   628 push @{$self->{reject}}, sub { _then_cb($new, $reject, 'reject', @_) };
  543         1183  
  88         166  
108              
109 543 100       1030 $self->_defer if $self->{results};
110              
111 543         1065 return $new;
112             }
113              
114 3     3 1 3774 sub timer { shift->_timer('resolve', @_) }
115 3     3 1 4812 sub timeout { shift->_timer('reject', @_) }
116              
117             sub wait {
118 46     46 1 83 my $self = shift;
119 46 50       113 return if (my $loop = $self->ioloop)->is_running;
120 46         72 my $done;
121 46     29   215 $self->_finally(0, sub { $done++; $loop->stop })->catch(sub { });
  46         69  
  46         143  
122 46         169 $loop->start until $done;
123             }
124              
125             sub _all {
126 18     18   48 my ($type, $class, @promises) = @_;
127              
128 18         53 my $all = $promises[0]->clone;
129 18         36 my $results = [];
130 18         33 my $remaining = scalar @promises;
131 18         51 for my $i (0 .. $#promises) {
132              
133             # "race"
134 62 100       155 if ($type == 1) {
    100          
    100          
135 8     3   21 $promises[$i]->then(sub { $all->resolve(@_); () }, sub { $all->reject(@_); () });
  5         9  
  5         5  
  3         12  
  3         6  
136             }
137              
138             # "all"
139             elsif ($type == 2) {
140             $promises[$i]->then(
141             sub {
142 36     36   77 $results->[$i] = [@_];
143 36 100       99 $all->resolve(@$results) if --$remaining <= 0;
144 36         56 return ();
145             },
146 4     4   10 sub { $all->reject(@_); () }
  4         5  
147 42         180 );
148             }
149              
150             # "any"
151             elsif ($type == 3) {
152             $promises[$i]->then(
153 2     2   5 sub { $all->resolve(@_); () },
  2         2  
154             sub {
155 4     4   19 $results->[$i] = [@_];
156 4 100       10 $all->reject(@$results) if --$remaining <= 0;
157 4         6 return ();
158             }
159 6         16 );
160             }
161              
162             # "all_settled"
163             else {
164             $promises[$i]->then(
165             sub {
166 5     5   19 $results->[$i] = {status => 'fulfilled', value => [@_]};
167 5 100       14 $all->resolve(@$results) if --$remaining <= 0;
168 5         7 return ();
169             },
170             sub {
171 1     1   4 $results->[$i] = {status => 'rejected', reason => [@_]};
172 1 50       4 $all->resolve(@$results) if --$remaining <= 0;
173 1         2 return ();
174             }
175 6         26 );
176             }
177             }
178              
179 18         116 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   952 my $self = shift;
191              
192 880 50       1500 return unless my $results = $self->{results};
193 880 100       1538 my $cbs = $self->{status} eq 'resolve' ? $self->{resolve} : $self->{reject};
194 880         1117 @{$self}{qw(cycle resolve reject)} = (undef, [], []);
  880         2651  
195              
196 880     880   1852 $self->ioloop->next_tick(sub { $_->(@$results) for @$cbs });
  880         2214  
197             }
198              
199             sub _finally {
200 57     57   126 my ($self, $handled, $finally) = @_;
201              
202 57         101 my $new = $self->clone;
203             my $cb = sub {
204 57     57   112 my @results = @_;
205 57         124 $new->resolve($finally->())->then(sub {@results});
  55         99  
206 57         173 };
207              
208 57         89 my $before = $self->{handled};
209 57         141 $self->catch($cb);
210 57         100 my $next = $self->then($cb);
211 57 100 100     242 delete $self->{handled} if !$before && !$handled;
212              
213 57         182 return $next;
214             }
215              
216             sub _settle {
217 877     877   1464 my ($self, $status, @results) = @_;
218              
219 877   100     2157 my $thenable = blessed $results[0] && $results[0]->can('then');
220 877 100       1545 unless (ref $self) {
221 38 50 66     126 return $results[0] if $thenable && $status eq 'resolve' && $results[0]->isa('Mojo::Promise');
      66        
222 34         63 $self = $self->new;
223             }
224              
225 873 100 100     2140 if ($thenable && $status eq 'resolve') {
    100          
226 67     46   244 $results[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () });
  61         132  
  61         91  
  6         13  
  6         6  
227             }
228             elsif (!$self->{results}) {
229 775         980 @{$self}{qw(results status)} = (\@results, $status);
  775         1561  
230 775         1238 $self->_defer;
231             }
232              
233 873         3741 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   915 my ($new, $cb, $method, @results) = @_;
246              
247 517 100       1061 return $new->$method(@results) unless defined $cb;
248              
249 351         355 my @res;
250 351 100       349 return $new->reject($@) unless eval { @res = $cb->(@results); 1 };
  351         640  
  349         921  
251 349         574 return $new->resolve(@res);
252             }
253              
254             sub _timer {
255 6     6   21 my ($self, $method, $after, @results) = @_;
256 6 100       22 $self = $self->new unless ref $self;
257 6 100 100     27 $results[0] = 'Promise timeout' if $method eq 'reject' && !@results;
258 6     6   19 $self->ioloop->timer($after => sub { $self->$method(@results) });
  6         64  
259 6         27 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