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   1435 use Mojo::Base -base;
  67         155  
  67         788  
3              
4 67     67   587 use Carp qw(carp croak);
  67         157  
  67         4873  
5 67     67   27344 use Mojo::Exception;
  67         222  
  67         3824  
6 67     67   1193 use Mojo::IOLoop;
  67         314  
  67         507  
7 67     67   409 use Scalar::Util qw(blessed);
  67         158  
  67         4775  
8              
9 67   50 67   412 use constant DEBUG => $ENV{MOJO_PROMISE_DEBUG} || 0;
  67         137  
  67         332363  
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   47586 my $self = shift;
53 798 100 100     9724 return if $self->{handled} || ($self->{status} // '') ne 'reject' || !$self->{results};
      100        
      66        
54 7         15 carp "Unhandled rejected promise: @{$self->{results}}";
  7         1428  
55 7         89 warn $self->{debug}->message("-- Destroyed promise\n")->verbose(1)->to_string if DEBUG;
56             }
57              
58 11     11 1 117 sub all { _all(2, @_) }
59 2     2 1 15 sub all_settled { _all(0, @_) }
60 2     2 1 10 sub any { _all(3, @_) }
61              
62 177     177 1 1481 sub catch { shift->then(undef, shift) }
63              
64 627     627 1 1430 sub clone { $_[0]->new->ioloop($_[0]->ioloop) }
65              
66 11     11 1 98 sub finally { shift->_finally(1, @_) }
67              
68             sub map {
69 4 100   4 1 15775 my ($class, $options, $cb, @items) = (shift, ref $_[0] eq 'HASH' ? shift : {}, @_);
70              
71 4 100 66     34 return $class->all(map { $_->$cb } @items) if !$options->{concurrency} || @items <= $options->{concurrency};
  8         14  
72              
73 2         10 my @start = map { $_->$cb } splice @items, 0, $options->{concurrency};
  6         18  
74 2         8 my @wait = map { $start[0]->clone } 0 .. $#items;
  8         17  
75              
76             my $start_next = sub {
77 9 100   9   25 return () unless @items;
78 6         11 my $item = shift @items;
79 6         15 my ($start_next, $chain) = (__SUB__, shift @wait);
80 6         25 $_->$cb->then(sub { $chain->resolve(@_); $start_next->() }, sub { $chain->reject(@_); @items = () }) for $item;
  6         13  
  6         14  
  0         0  
  0         0  
81 6         13 return ();
82 2         10 };
83              
84 2     3   9 $_->then($start_next, sub { }) for @start;
85              
86 2         24 return $class->all(@start, @wait);
87             }
88              
89             sub new {
90 804     804 1 401041 my $self = shift->SUPER::new;
91 804         1029 $self->{debug} = Mojo::Exception->new->trace if DEBUG;
92 804 100   1   1540 shift->(sub { $self->resolve(@_) }, sub { $self->reject(@_) }) if @_;
  1         9  
  1         7  
93 804         2977 return $self;
94             }
95              
96 3     3 1 20 sub race { _all(1, @_) }
97              
98 99     99 1 8163 sub reject { shift->_settle('reject', @_) }
99 778     778 1 10147 sub resolve { shift->_settle('resolve', @_) }
100              
101             sub then {
102 543     543 1 1291 my ($self, $resolve, $reject) = @_;
103              
104 543         1641 my $new = $self->clone;
105 543         1158 $self->{handled} = 1;
106 543     429   830 push @{$self->{resolve}}, sub { _then_cb($new, $resolve, 'resolve', @_) };
  543         2632  
  429         1049  
107 543     88   909 push @{$self->{reject}}, sub { _then_cb($new, $reject, 'reject', @_) };
  543         1758  
  88         236  
108              
109 543 100       3844 $self->_defer if $self->{results};
110              
111 543         2134 return $new;
112             }
113              
114 3     3 1 6609 sub timer { shift->_timer('resolve', @_) }
115 3     3 1 7605 sub timeout { shift->_timer('reject', @_) }
116              
117             sub wait {
118 46     46 1 121 my $self = shift;
119 46 50       150 return if (my $loop = $self->ioloop)->is_running;
120 46         105 my $done;
121 46     27   270 $self->_finally(0, sub { $done++; $loop->stop })->catch(sub { });
  46         86  
  46         194  
122 46         282 $loop->start until $done;
123             }
124              
125             sub _all {
126 18     18   62 my ($type, $class, @promises) = @_;
127              
128 18         74 my $all = $promises[0]->clone;
129 18         42 my $results = [];
130 18         43 my $remaining = scalar @promises;
131 18         74 for my $i (0 .. $#promises) {
132              
133             # "race"
134 62 100       170 if ($type == 1) {
    100          
    100          
135 8     5   25 $promises[$i]->then(sub { $all->resolve(@_); () }, sub { $all->reject(@_); () });
  5         14  
  5         8  
  3         11  
  3         6  
136             }
137              
138             # "all"
139             elsif ($type == 2) {
140             $promises[$i]->then(
141             sub {
142 36     36   94 $results->[$i] = [@_];
143 36 100       129 $all->resolve(@$results) if --$remaining <= 0;
144 36         61 return ();
145             },
146 4     4   13 sub { $all->reject(@_); () }
  4         7  
147 42         318 );
148             }
149              
150             # "any"
151             elsif ($type == 3) {
152             $promises[$i]->then(
153 2     2   5 sub { $all->resolve(@_); () },
  2         3  
154             sub {
155 4     4   9 $results->[$i] = [@_];
156 4 100       9 $all->reject(@$results) if --$remaining <= 0;
157 4         5 return ();
158             }
159 6         20 );
160             }
161              
162             # "all_settled"
163             else {
164             $promises[$i]->then(
165             sub {
166 5     5   23 $results->[$i] = {status => 'fulfilled', value => [@_]};
167 5 100       19 $all->resolve(@$results) if --$remaining <= 0;
168 5         9 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         35 );
176             }
177             }
178              
179 18         155 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   1287 my $self = shift;
191              
192 880 50       4940 return unless my $results = $self->{results};
193 880 100       2018 my $cbs = $self->{status} eq 'resolve' ? $self->{resolve} : $self->{reject};
194 880         1770 @{$self}{qw(cycle resolve reject)} = (undef, [], []);
  880         3872  
195              
196 880     880   2492 $self->ioloop->next_tick(sub { $_->(@$results) for @$cbs });
  880         3154  
197             }
198              
199             sub _finally {
200 57     57   169 my ($self, $handled, $finally) = @_;
201              
202 57         154 my $new = $self->clone;
203             my $cb = sub {
204 57     57   171 my @results = @_;
205 57         155 $new->resolve($finally->())->then(sub {@results});
  55         133  
206 57         324 };
207              
208 57         133 my $before = $self->{handled};
209 57         239 $self->catch($cb);
210 57         147 my $next = $self->then($cb);
211 57 100 100     324 delete $self->{handled} if !$before && !$handled;
212              
213 57         310 return $next;
214             }
215              
216             sub _settle {
217 877     877   1962 my ($self, $status, @results) = @_;
218              
219 877   100     6047 my $thenable = blessed $results[0] && $results[0]->can('then');
220 877 100       1896 unless (ref $self) {
221 38 50 66     162 return $results[0] if $thenable && $status eq 'resolve' && $results[0]->isa('Mojo::Promise');
      66        
222 34         89 $self = $self->new;
223             }
224              
225 873 100 100     3154 if ($thenable && $status eq 'resolve') {
    100          
226 67     21   388 $results[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () });
  61         199  
  61         148  
  6         19  
  6         10  
227             }
228             elsif (!$self->{results}) {
229 775         1306 @{$self}{qw(results status)} = (\@results, $status);
  775         2936  
230 775         1636 $self->_defer;
231             }
232              
233 873         5378 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   1184 my ($new, $cb, $method, @results) = @_;
246              
247 517 100       1334 return $new->$method(@results) unless defined $cb;
248              
249 351         1096 my @res;
250 351 100       511 return $new->reject($@) unless eval { @res = $cb->(@results); 1 };
  351         820  
  349         1209  
251 349         926 return $new->resolve(@res);
252             }
253              
254             sub _timer {
255 6     6   24 my ($self, $method, $after, @results) = @_;
256 6 100       29 $self = $self->new unless ref $self;
257 6 100 100     29 $results[0] = 'Promise timeout' if $method eq 'reject' && !@results;
258 6     6   23 $self->ioloop->timer($after => sub { $self->$method(@results) });
  6         71  
259 6         35 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