File Coverage

blib/lib/Mojo/Promise.pm
Criterion Covered Total %
statement 152 186 81.7
branch 46 60 76.6
condition 26 42 61.9
subroutine 45 62 72.5
pod 15 27 55.5
total 284 377 75.3


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