1 | package Mojo::Promise;
|
---|
2 | use Mojo::Base -base;
|
---|
3 |
|
---|
4 | use Mojo::IOLoop;
|
---|
5 | use Scalar::Util qw(blessed weaken);
|
---|
6 |
|
---|
7 | has ioloop => sub { Mojo::IOLoop->singleton };
|
---|
8 |
|
---|
9 | sub all {
|
---|
10 | my ($class, @promises) = @_;
|
---|
11 |
|
---|
12 | my $all = $class->new;
|
---|
13 | my $results = [];
|
---|
14 | my $remaining = scalar @promises;
|
---|
15 | for my $i (0 .. $#promises) {
|
---|
16 | $promises[$i]->then(
|
---|
17 | sub {
|
---|
18 | $results->[$i] = [@_];
|
---|
19 | $all->resolve(@$results) if --$remaining <= 0;
|
---|
20 | },
|
---|
21 | sub { $all->reject(@_) }
|
---|
22 | );
|
---|
23 | }
|
---|
24 |
|
---|
25 | return @promises ? $all : $all->resolve;
|
---|
26 | }
|
---|
27 |
|
---|
28 | sub catch { shift->then(undef, shift) }
|
---|
29 |
|
---|
30 | sub finally {
|
---|
31 | my ($self, $finally) = @_;
|
---|
32 |
|
---|
33 | my $new = $self->_clone;
|
---|
34 | push @{$self->{resolve}}, sub { _finally($new, $finally, 'resolve', @_) };
|
---|
35 | push @{$self->{reject}}, sub { _finally($new, $finally, 'reject', @_) };
|
---|
36 |
|
---|
37 | $self->_defer if $self->{result};
|
---|
38 |
|
---|
39 | return $new;
|
---|
40 | }
|
---|
41 |
|
---|
42 | sub race {
|
---|
43 | my ($class, @promises) = @_;
|
---|
44 | my $new = $class->new;
|
---|
45 | $_->then(sub { $new->resolve(@_) }, sub { $new->reject(@_) }) for @promises;
|
---|
46 | return $new;
|
---|
47 | }
|
---|
48 |
|
---|
49 | sub reject { shift->_settle('reject', @_) }
|
---|
50 | sub resolve { shift->_settle('resolve', @_) }
|
---|
51 |
|
---|
52 | sub then {
|
---|
53 | my ($self, $resolve, $reject) = @_;
|
---|
54 |
|
---|
55 | my $new = $self->_clone;
|
---|
56 | push @{$self->{resolve}}, sub { _then($new, $resolve, 'resolve', @_) };
|
---|
57 | push @{$self->{reject}}, sub { _then($new, $reject, 'reject', @_) };
|
---|
58 |
|
---|
59 | $self->_defer if $self->{result};
|
---|
60 |
|
---|
61 | return $new;
|
---|
62 | }
|
---|
63 |
|
---|
64 | sub wait {
|
---|
65 | my $self = shift;
|
---|
66 | return if (my $loop = $self->ioloop)->is_running;
|
---|
67 | $self->finally(sub { $loop->stop });
|
---|
68 | $loop->start;
|
---|
69 | }
|
---|
70 |
|
---|
71 | sub _clone {
|
---|
72 | my $self = shift;
|
---|
73 | my $clone = $self->new;
|
---|
74 | weaken $clone->ioloop($self->ioloop)->{ioloop};
|
---|
75 | return $clone;
|
---|
76 | }
|
---|
77 |
|
---|
78 | sub _defer {
|
---|
79 | my $self = shift;
|
---|
80 |
|
---|
81 | return unless my $result = $self->{result};
|
---|
82 | my $cbs = $self->{status} eq 'resolve' ? $self->{resolve} : $self->{reject};
|
---|
83 | @{$self}{qw(resolve reject)} = ([], []);
|
---|
84 |
|
---|
85 | $self->ioloop->next_tick(sub { $_->(@$result) for @$cbs });
|
---|
86 | }
|
---|
87 |
|
---|
88 | sub _finally {
|
---|
89 | my ($new, $finally, $method, @result) = @_;
|
---|
90 | my ($res) = eval { $finally->(@result) };
|
---|
91 | return $new->$method(@result)
|
---|
92 | unless $res && blessed $res && $res->can('then');
|
---|
93 | $res->then(sub { $new->$method(@result) }, sub { $new->$method(@result) });
|
---|
94 | }
|
---|
95 |
|
---|
96 | sub _settle {
|
---|
97 | my ($self, $status) = (shift, shift);
|
---|
98 |
|
---|
99 | $_[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () })
|
---|
100 | and return $self
|
---|
101 | if blessed $_[0] && $_[0]->can('then');
|
---|
102 |
|
---|
103 | return $self if $self->{result};
|
---|
104 |
|
---|
105 | @{$self}{qw(result status)} = ([@_], $status);
|
---|
106 | $self->_defer;
|
---|
107 | return $self;
|
---|
108 | }
|
---|
109 |
|
---|
110 | sub _then {
|
---|
111 | my ($new, $cb, $method, @result) = @_;
|
---|
112 |
|
---|
113 | return $new->$method(@result) unless defined $cb;
|
---|
114 |
|
---|
115 | my @res;
|
---|
116 | return $new->reject($@) unless eval { @res = $cb->(@result); 1 };
|
---|
117 | return $new->resolve(@res);
|
---|
118 | }
|
---|
119 |
|
---|
120 | 1;
|
---|
121 |
|
---|
122 | =encoding utf8
|
---|
123 |
|
---|
124 | =head1 NAME
|
---|
125 |
|
---|
126 | Mojo::Promise - Promises/A+
|
---|
127 |
|
---|
128 | =head1 SYNOPSIS
|
---|
129 |
|
---|
130 | use Mojo::Promise;
|
---|
131 | use Mojo::UserAgent;
|
---|
132 |
|
---|
133 | # Wrap continuation-passing style APIs with promises
|
---|
134 | my $ua = Mojo::UserAgent->new;
|
---|
135 | sub get {
|
---|
136 | my $promise = Mojo::Promise->new;
|
---|
137 | $ua->get(@_ => sub {
|
---|
138 | my ($ua, $tx) = @_;
|
---|
139 | my $err = $tx->error;
|
---|
140 | $promise->resolve($tx) if !$err || $err->{code};
|
---|
141 | $promise->reject($err->{message});
|
---|
142 | });
|
---|
143 | return $promise;
|
---|
144 | }
|
---|
145 |
|
---|
146 | # Perform non-blocking operations sequentially
|
---|
147 | get('https://mojolicious.org')->then(sub {
|
---|
148 | my $mojo = shift;
|
---|
149 | say $mojo->res->code;
|
---|
150 | return get('https://metacpan.org');
|
---|
151 | })->then(sub {
|
---|
152 | my $cpan = shift;
|
---|
153 | say $cpan->res->code;
|
---|
154 | })->catch(sub {
|
---|
155 | my $err = shift;
|
---|
156 | warn "Something went wrong: $err";
|
---|
157 | })->wait;
|
---|
158 |
|
---|
159 | # Synchronize non-blocking operations (all)
|
---|
160 | my $mojo = get('https://mojolicious.org');
|
---|
161 | my $cpan = get('https://metacpan.org');
|
---|
162 | Mojo::Promise->all($mojo, $cpan)->then(sub {
|
---|
163 | my ($mojo, $cpan) = @_;
|
---|
164 | say $mojo->[0]->res->code;
|
---|
165 | say $cpan->[0]->res->code;
|
---|
166 | })->catch(sub {
|
---|
167 | my $err = shift;
|
---|
168 | warn "Something went wrong: $err";
|
---|
169 | })->wait;
|
---|
170 |
|
---|
171 | # Synchronize non-blocking operations (race)
|
---|
172 | my $mojo = get('https://mojolicious.org');
|
---|
173 | my $cpan = get('https://metacpan.org');
|
---|
174 | Mojo::Promise->race($mojo, $cpan)->then(sub {
|
---|
175 | my $tx = shift;
|
---|
176 | say $tx->req->url, ' won!';
|
---|
177 | })->catch(sub {
|
---|
178 | my $err = shift;
|
---|
179 | warn "Something went wrong: $err";
|
---|
180 | })->wait;
|
---|
181 |
|
---|
182 | =head1 DESCRIPTION
|
---|
183 |
|
---|
184 | L<Mojo::Promise> is a Perl-ish implementation of
|
---|
185 | L<Promises/A+|https://promisesaplus.com>.
|
---|
186 |
|
---|
187 | =head1 STATES
|
---|
188 |
|
---|
189 | A promise is an object representing the eventual completion or failure of a
|
---|
190 | non-blocking operation. It allows non-blocking functions to return values, like
|
---|
191 | blocking functions. But instead of immediately returning the final value, the
|
---|
192 | non-blocking function returns a promise to supply the value at some point in the
|
---|
193 | future.
|
---|
194 |
|
---|
195 | A promise can be in one of three states:
|
---|
196 |
|
---|
197 | =over 2
|
---|
198 |
|
---|
199 | =item pending
|
---|
200 |
|
---|
201 | Initial state, neither fulfilled nor rejected.
|
---|
202 |
|
---|
203 | =item fulfilled
|
---|
204 |
|
---|
205 | Meaning that the operation completed successfully.
|
---|
206 |
|
---|
207 | =item rejected
|
---|
208 |
|
---|
209 | Meaning that the operation failed.
|
---|
210 |
|
---|
211 | =back
|
---|
212 |
|
---|
213 | A pending promise can either be fulfilled with a value or rejected with a
|
---|
214 | reason. When either happens, the associated handlers queued up by a promise's
|
---|
215 | L</"then"> method are called.
|
---|
216 |
|
---|
217 | =head1 ATTRIBUTES
|
---|
218 |
|
---|
219 | L<Mojo::Promise> implements the following attributes.
|
---|
220 |
|
---|
221 | =head2 ioloop
|
---|
222 |
|
---|
223 | my $loop = $promise->ioloop;
|
---|
224 | $promise = $promise->ioloop(Mojo::IOLoop->new);
|
---|
225 |
|
---|
226 | Event loop object to control, defaults to the global L<Mojo::IOLoop> singleton.
|
---|
227 |
|
---|
228 | =head1 METHODS
|
---|
229 |
|
---|
230 | L<Mojo::Promise> inherits all methods from L<Mojo::Base> and implements
|
---|
231 | the following new ones.
|
---|
232 |
|
---|
233 | =head2 all
|
---|
234 |
|
---|
235 | my $new = Mojo::Promise->all(@promises);
|
---|
236 |
|
---|
237 | Returns a new L<Mojo::Promise> object that either fulfills when all of the
|
---|
238 | passed L<Mojo::Promise> objects have fulfilled or rejects as soon as one of them
|
---|
239 | rejects. If the returned promise fulfills, it is fulfilled with the values from
|
---|
240 | the fulfilled promises in the same order as the passed promises. This method can
|
---|
241 | be useful for aggregating results of multiple promises.
|
---|
242 |
|
---|
243 | =head2 catch
|
---|
244 |
|
---|
245 | my $new = $promise->catch(sub {...});
|
---|
246 |
|
---|
247 | Appends a rejection handler callback to the promise, and returns a new
|
---|
248 | L<Mojo::Promise> object resolving to the return value of the callback if it is
|
---|
249 | called, or to its original fulfillment value if the promise is instead
|
---|
250 | fulfilled.
|
---|
251 |
|
---|
252 | # Longer version
|
---|
253 | my $new = $promise->then(undef, sub {...});
|
---|
254 |
|
---|
255 | # Pass along the rejection reason
|
---|
256 | $promise->catch(sub {
|
---|
257 | my @reason = @_;
|
---|
258 | warn "Something went wrong: $reason[0]";
|
---|
259 | return @reason;
|
---|
260 | });
|
---|
261 |
|
---|
262 | # Change the rejection reason
|
---|
263 | $promise->catch(sub {
|
---|
264 | my @reason = @_;
|
---|
265 | return "This is bad: $reason[0]";
|
---|
266 | });
|
---|
267 |
|
---|
268 | =head2 finally
|
---|
269 |
|
---|
270 | my $new = $promise->finally(sub {...});
|
---|
271 |
|
---|
272 | Appends a fulfillment and rejection handler to the promise, and returns a new
|
---|
273 | L<Mojo::Promise> object resolving to the original fulfillment value or rejection
|
---|
274 | reason.
|
---|
275 |
|
---|
276 | # Do something on fulfillment and rejection
|
---|
277 | $promise->finally(sub {
|
---|
278 | my @value_or_reason = @_;
|
---|
279 | say "We are done!";
|
---|
280 | });
|
---|
281 |
|
---|
282 | =head2 race
|
---|
283 |
|
---|
284 | my $new = Mojo::Promise->race(@promises);
|
---|
285 |
|
---|
286 | Returns a new L<Mojo::Promise> object that fulfills or rejects as soon as one of
|
---|
287 | the passed L<Mojo::Promise> objects fulfills or rejects, with the value or
|
---|
288 | reason from that promise.
|
---|
289 |
|
---|
290 | =head2 reject
|
---|
291 |
|
---|
292 | $promise = $promise->reject(@reason);
|
---|
293 |
|
---|
294 | Reject the promise with one or more rejection reasons.
|
---|
295 |
|
---|
296 | # Generate rejected promise
|
---|
297 | my $promise = Mojo::Promise->new->reject('Something went wrong: Oops');
|
---|
298 |
|
---|
299 | =head2 resolve
|
---|
300 |
|
---|
301 | $promise = $promise->resolve(@value);
|
---|
302 |
|
---|
303 | Resolve the promise with one or more fulfillment values.
|
---|
304 |
|
---|
305 | # Generate fulfilled promise
|
---|
306 | my $promise = Mojo::Promise->new->resolve('The result is: 24');
|
---|
307 |
|
---|
308 | =head2 then
|
---|
309 |
|
---|
310 | my $new = $promise->then(sub {...});
|
---|
311 | my $new = $promise->then(sub {...}, sub {...});
|
---|
312 | my $new = $promise->then(undef, sub {...});
|
---|
313 |
|
---|
314 | Appends fulfillment and rejection handlers to the promise, and returns a new
|
---|
315 | L<Mojo::Promise> object resolving to the return value of the called handler.
|
---|
316 |
|
---|
317 | # Pass along the fulfillment value or rejection reason
|
---|
318 | $promise->then(
|
---|
319 | sub {
|
---|
320 | my @value = @_;
|
---|
321 | say "The result is $value[0]";
|
---|
322 | return @value;
|
---|
323 | },
|
---|
324 | sub {
|
---|
325 | my @reason = @_;
|
---|
326 | warn "Something went wrong: $reason[0]";
|
---|
327 | return @reason;
|
---|
328 | }
|
---|
329 | );
|
---|
330 |
|
---|
331 | # Change the fulfillment value or rejection reason
|
---|
332 | $promise->then(
|
---|
333 | sub {
|
---|
334 | my @value = @_;
|
---|
335 | return "This is good: $value[0]";
|
---|
336 | },
|
---|
337 | sub {
|
---|
338 | my @reason = @_;
|
---|
339 | return "This is bad: $reason[0]";
|
---|
340 | }
|
---|
341 | );
|
---|
342 |
|
---|
343 | =head2 wait
|
---|
344 |
|
---|
345 | $promise->wait;
|
---|
346 |
|
---|
347 | Start L</"ioloop"> and stop it again once the promise has been fulfilled or
|
---|
348 | rejected, does nothing when L</"ioloop"> is already running.
|
---|
349 |
|
---|
350 | =head1 SEE ALSO
|
---|
351 |
|
---|
352 | L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
|
---|
353 |
|
---|
354 | =cut
|
---|