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   808 use Mojo::Base -base;
  67         147  
  67         434  
3              
4 67     67   346 use Carp qw(carp croak);
  67         107  
  67         3322  
5 67     67   19793 use Mojo::Exception;
  67         161  
  67         2822  
6 67     67   806 use Mojo::IOLoop;
  67         99  
  67         414  
7 67     67   240 use Scalar::Util qw(blessed);
  67         99  
  67         3247  
8              
9 67   50 67   260 use constant DEBUG => $ENV{MOJO_PROMISE_DEBUG} || 0;
  67         120  
  67         204717  
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 806     806   28291 my $self = shift;
53 806 100 100     6290 return if $self->{handled} || ($self->{status} // '') ne 'reject' || !$self->{results};
      100        
      66        
54 7         10 carp "Unhandled rejected promise: @{$self->{results}}";
  7         1005  
55 7         65 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 10 sub all_settled { _all(0, @_) }
60 2     2 1 9 sub any { _all(3, @_) }
61              
62 179     179 1 1107 sub catch { shift->then(undef, shift) }
63              
64 634     634 1 957 sub clone { $_[0]->new->ioloop($_[0]->ioloop) }
65              
66 11     11 1 75 sub finally { shift->_finally(1, @_) }
67              
68             sub map {
69 4 100   4 1 8918 my ($class, $options, $cb, @items) = (shift, ref $_[0] eq 'HASH' ? shift : {}, @_);
70              
71 4 100 66     19 return $class->all(map { $_->$cb } @items) if !$options->{concurrency} || @items <= $options->{concurrency};
  8         16  
72              
73 2         6 my @start = map { $_->$cb } splice @items, 0, $options->{concurrency};
  6         12  
74 2         8 my @wait = map { $start[0]->clone } 0 .. $#items;
  8         10  
75              
76             my $start_next = sub {
77 9 100   9   14 return () unless @items;
78 6         8 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         11  
  6         11  
  0         0  
  0         0  
81 6         8 return ();
82 2         8 };
83              
84 2     3   6 $_->then($start_next, sub { }) for @start;
85              
86 2         7 return $class->all(@start, @wait);
87             }
88              
89             sub new {
90 812     812 1 261182 my $self = shift->SUPER::new;
91 812         908 $self->{debug} = Mojo::Exception->new->trace if DEBUG;
92 812 100   1   1245 shift->(sub { $self->resolve(@_) }, sub { $self->reject(@_) }) if @_;
  1         4  
  1         5  
93 812         1401 return $self;
94             }
95              
96 3     3 1 15 sub race { _all(1, @_) }
97              
98 99     99 1 5743 sub reject { shift->_settle('reject', @_) }
99 787     787 1 5356 sub resolve { shift->_settle('resolve', @_) }
100              
101             sub then {
102 549     549 1 919 my ($self, $resolve, $reject) = @_;
103              
104 549         833 my $new = $self->clone;
105 549         767 $self->{handled} = 1;
106 549     435   566 push @{$self->{resolve}}, sub { _then_cb($new, $resolve, 'resolve', @_) };
  549         1434  
  435         672  
107 549     88   615 push @{$self->{reject}}, sub { _then_cb($new, $reject, 'reject', @_) };
  549         1355  
  88         156  
108              
109 549 100       980 $self->_defer if $self->{results};
110              
111 549         1009 return $new;
112             }
113              
114 3     3 1 3642 sub timer { shift->_timer('resolve', @_) }
115 3     3 1 5524 sub timeout { shift->_timer('reject', @_) }
116              
117             sub wait {
118 47     47 1 85 my $self = shift;
119 47 50       99 return if (my $loop = $self->ioloop)->is_running;
120 47         81 my $done;
121 47     26   185 $self->_finally(0, sub { $done++; $loop->stop })->catch(sub { });
  47         60  
  47         131  
122 47         169 $loop->start until $done;
123             }
124              
125             sub _all {
126 18     18   38 my ($type, $class, @promises) = @_;
127              
128 18         49 my $all = $promises[0]->clone;
129 18         31 my $results = [];
130 18         28 my $remaining = scalar @promises;
131 18         53 for my $i (0 .. $#promises) {
132              
133             # "race"
134 62 100       129 if ($type == 1) {
    100          
    100          
135 8     5   22 $promises[$i]->then(sub { $all->resolve(@_); () }, sub { $all->reject(@_); () });
  5         9  
  5         5  
  3         12  
  3         8  
136             }
137              
138             # "all"
139             elsif ($type == 2) {
140             $promises[$i]->then(
141             sub {
142 36     36   68 $results->[$i] = [@_];
143 36 100       76 $all->resolve(@$results) if --$remaining <= 0;
144 36         51 return ();
145             },
146 4     4   10 sub { $all->reject(@_); () }
  4         4  
147 42         159 );
148             }
149              
150             # "any"
151             elsif ($type == 3) {
152             $promises[$i]->then(
153 2     2   4 sub { $all->resolve(@_); () },
  2         2  
154             sub {
155 4     4   10 $results->[$i] = [@_];
156 4 100       10 $all->reject(@$results) if --$remaining <= 0;
157 4         6 return ();
158             }
159 6         40 );
160             }
161              
162             # "all_settled"
163             else {
164             $promises[$i]->then(
165             sub {
166 5     5   13 $results->[$i] = {status => 'fulfilled', value => [@_]};
167 5 100       13 $all->resolve(@$results) if --$remaining <= 0;
168 5         6 return ();
169             },
170             sub {
171 1     1   3 $results->[$i] = {status => 'rejected', reason => [@_]};
172 1 50       3 $all->resolve(@$results) if --$remaining <= 0;
173 1         4 return ();
174             }
175 6         20 );
176             }
177             }
178              
179 18         117 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 889     889   924 my $self = shift;
191              
192 889 50       1428 return unless my $results = $self->{results};
193 889 100       1483 my $cbs = $self->{status} eq 'resolve' ? $self->{resolve} : $self->{reject};
194 889         1162 @{$self}{qw(cycle resolve reject)} = (undef, [], []);
  889         2689  
195              
196 889     889   1762 $self->ioloop->next_tick(sub { $_->(@$results) for @$cbs });
  889         2076  
197             }
198              
199             sub _finally {
200 58     58   105 my ($self, $handled, $finally) = @_;
201              
202 58         98 my $new = $self->clone;
203             my $cb = sub {
204 58     58   117 my @results = @_;
205 58         106 $new->resolve($finally->())->then(sub {@results});
  56         87  
206 58         205 };
207              
208 58         90 my $before = $self->{handled};
209 58         142 $self->catch($cb);
210 58         92 my $next = $self->then($cb);
211 58 100 100     222 delete $self->{handled} if !$before && !$handled;
212              
213 58         204 return $next;
214             }
215              
216             sub _settle {
217 886     886   1460 my ($self, $status, @results) = @_;
218              
219 886   100     2191 my $thenable = blessed $results[0] && $results[0]->can('then');
220 886 100       1413 unless (ref $self) {
221 38 50 66     133 return $results[0] if $thenable && $status eq 'resolve' && $results[0]->isa('Mojo::Promise');
      66        
222 34         63 $self = $self->new;
223             }
224              
225 882 100 100     2017 if ($thenable && $status eq 'resolve') {
    100          
226 68     56   271 $results[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () });
  62         135  
  62         77  
  6         11  
  6         7  
227             }
228             elsif (!$self->{results}) {
229 783         920 @{$self}{qw(results status)} = (\@results, $status);
  783         1556  
230 783         1232 $self->_defer;
231             }
232              
233 882         3773 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 523     523   847 my ($new, $cb, $method, @results) = @_;
246              
247 523 100       944 return $new->$method(@results) unless defined $cb;
248              
249 355         324 my @res;
250 355 100       334 return $new->reject($@) unless eval { @res = $cb->(@results); 1 };
  355         600  
  353         849  
251 353         561 return $new->resolve(@res);
252             }
253              
254             sub _timer {
255 6     6   17 my ($self, $method, $after, @results) = @_;
256 6 100       19 $self = $self->new unless ref $self;
257 6 100 100     21 $results[0] = 'Promise timeout' if $method eq 'reject' && !@results;
258 6     6   13 $self->ioloop->timer($after => sub { $self->$method(@results) });
  6         47  
259 6         26 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