PageRenderTime 64ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Mojo/Promise.pm

http://github.com/kraih/mojo
Perl | 560 lines | 430 code | 115 blank | 15 comment | 31 complexity | d3aa70b1dd1137cd96890728479273d9 MD5 | raw file
Possible License(s): AGPL-3.0
  1. package Mojo::Promise;
  2. use Mojo::Base -base;
  3. use Mojo::IOLoop;
  4. use Mojo::Util qw(deprecated);
  5. use Scalar::Util qw(blessed);
  6. has ioloop => sub { Mojo::IOLoop->singleton }, weak => 1;
  7. sub AWAIT_CLONE { _await('clone', @_) }
  8. sub AWAIT_DONE { shift->resolve(@_) }
  9. sub AWAIT_FAIL { shift->reject(@_) }
  10. sub AWAIT_GET {
  11. my $self = shift;
  12. my @results = @{$self->{result} // []};
  13. die $results[0] unless $self->{status} eq 'resolve';
  14. return wantarray ? @results : $results[0];
  15. }
  16. sub AWAIT_IS_CANCELLED {undef}
  17. sub AWAIT_IS_READY {
  18. my $self = shift;
  19. return !!$self->{result} && !@{$self->{resolve}} && !@{$self->{reject}};
  20. }
  21. sub AWAIT_NEW_DONE { _await('resolve', @_) }
  22. sub AWAIT_NEW_FAIL { _await('reject', @_) }
  23. sub AWAIT_ON_CANCEL { }
  24. sub AWAIT_ON_READY { shift->finally(@_) }
  25. sub all { _all(2, @_) }
  26. sub all_settled { _all(0, @_) }
  27. sub any { _all(3, @_) }
  28. sub catch { shift->then(undef, shift) }
  29. sub clone { $_[0]->new->ioloop($_[0]->ioloop) }
  30. sub finally {
  31. my ($self, $finally) = @_;
  32. my $new = $self->clone;
  33. push @{$self->{resolve}}, sub { _finally($new, $finally, 'resolve', @_) };
  34. push @{$self->{reject}}, sub { _finally($new, $finally, 'reject', @_) };
  35. $self->_defer if $self->{result};
  36. return $new;
  37. }
  38. sub map {
  39. my ($class, $options) = (shift, ref $_[0] eq 'HASH' ? shift : {});
  40. my ($cb, @items) = @_;
  41. my @start = map { $_->$cb } splice @items, 0,
  42. $options->{concurrency} // @items;
  43. my $proto = $class->resolve($start[0]);
  44. my (@trigger, @wait);
  45. for my $item (@items) {
  46. my $p = $proto->clone;
  47. push @trigger, $p;
  48. push @wait, $p->then(sub { local $_ = $item; $_->$cb });
  49. }
  50. my @all = map {
  51. $proto->clone->resolve($_)->then(
  52. sub { shift(@trigger)->resolve if @trigger; @_ },
  53. sub { @trigger = (); $proto->clone->reject($_[0]) },
  54. )
  55. } (@start, @wait);
  56. return $class->all(@all);
  57. }
  58. sub new {
  59. # DEPRECATED!
  60. if (@_ > 2 or ref($_[1]) eq 'HASH') {
  61. deprecated 'Mojo::Promise::new with attributes is DEPRECATED';
  62. return shift->SUPER::new(@_);
  63. }
  64. my $self = shift->SUPER::new;
  65. shift->(sub { $self->resolve(@_) }, sub { $self->reject(@_) }) if @_;
  66. return $self;
  67. }
  68. sub race { _all(1, @_) }
  69. sub reject { shift->_settle('reject', @_) }
  70. sub resolve { shift->_settle('resolve', @_) }
  71. sub then {
  72. my ($self, $resolve, $reject) = @_;
  73. my $new = $self->clone;
  74. push @{$self->{resolve}}, sub { _then($new, $resolve, 'resolve', @_) };
  75. push @{$self->{reject}}, sub { _then($new, $reject, 'reject', @_) };
  76. $self->_defer if $self->{result};
  77. return $new;
  78. }
  79. sub timer { shift->_timer('resolve', @_) }
  80. sub timeout { shift->_timer('reject', @_) }
  81. sub wait {
  82. my $self = shift;
  83. return if (my $loop = $self->ioloop)->is_running;
  84. my $done;
  85. $self->finally(sub { $done++; $loop->stop });
  86. $loop->start until $done;
  87. }
  88. sub _all {
  89. my ($type, $class, @promises) = @_;
  90. my $all = $promises[0]->clone;
  91. my $results = [];
  92. my $remaining = scalar @promises;
  93. for my $i (0 .. $#promises) {
  94. # "race"
  95. if ($type == 1) {
  96. $promises[$i]->then(sub { $all->resolve(@_) }, sub { $all->reject(@_) });
  97. }
  98. # "all"
  99. elsif ($type == 2) {
  100. $promises[$i]->then(
  101. sub {
  102. $results->[$i] = [@_];
  103. $all->resolve(@$results) if --$remaining <= 0;
  104. },
  105. sub { $all->reject(@_) }
  106. );
  107. }
  108. # "any"
  109. elsif ($type == 3) {
  110. $promises[$i]->then(
  111. sub { $all->resolve(@_) },
  112. sub {
  113. $results->[$i] = [@_];
  114. $all->reject(@$results) if --$remaining <= 0;
  115. }
  116. );
  117. }
  118. # "all_settled"
  119. else {
  120. $promises[$i]->then(
  121. sub {
  122. $results->[$i] = {status => 'fulfilled', value => [@_]};
  123. $all->resolve(@$results) if --$remaining <= 0;
  124. },
  125. sub {
  126. $results->[$i] = {status => 'rejected', reason => [@_]};
  127. $all->resolve(@$results) if --$remaining <= 0;
  128. }
  129. );
  130. }
  131. }
  132. return $all;
  133. }
  134. sub _await {
  135. my ($method, $class) = (shift, shift);
  136. my $promise = $class->$method(@_);
  137. $promise->{cycle} = $promise;
  138. return $promise;
  139. }
  140. sub _defer {
  141. my $self = shift;
  142. return unless my $result = $self->{result};
  143. my $cbs = $self->{status} eq 'resolve' ? $self->{resolve} : $self->{reject};
  144. @{$self}{qw(cycle resolve reject)} = (undef, [], []);
  145. $self->ioloop->next_tick(sub { $_->(@$result) for @$cbs });
  146. }
  147. sub _finally {
  148. my ($new, $finally, $method, @result) = @_;
  149. return $new->reject($@) unless eval { $finally->(); 1 };
  150. return $new->$method(@result);
  151. }
  152. sub _settle {
  153. my ($self, $status) = (shift, shift);
  154. my $thenable = blessed $_[0] && $_[0]->can('then');
  155. $self = $thenable ? $_[0]->clone : $self->new unless ref $self;
  156. $_[0]->then(sub { $self->resolve(@_); () }, sub { $self->reject(@_); () })
  157. and return $self
  158. if $thenable;
  159. return $self if $self->{result};
  160. @{$self}{qw(result status)} = ([@_], $status);
  161. $self->_defer;
  162. return $self;
  163. }
  164. sub _then {
  165. my ($new, $cb, $method, @result) = @_;
  166. return $new->$method(@result) unless defined $cb;
  167. my @res;
  168. return $new->reject($@) unless eval { @res = $cb->(@result); 1 };
  169. return $new->resolve(@res);
  170. }
  171. sub _timer {
  172. my ($self, $method, $after, @result) = @_;
  173. $self = $self->new unless ref $self;
  174. $result[0] = 'Promise timeout' if $method eq 'reject' && !@result;
  175. $self->ioloop->timer($after => sub { $self->$method(@result) });
  176. return $self;
  177. }
  178. 1;
  179. =encoding utf8
  180. =head1 NAME
  181. Mojo::Promise - Promises/A+
  182. =head1 SYNOPSIS
  183. use Mojo::Promise;
  184. use Mojo::UserAgent;
  185. # Wrap continuation-passing style APIs with promises
  186. my $ua = Mojo::UserAgent->new;
  187. sub get_p {
  188. my $promise = Mojo::Promise->new;
  189. $ua->get(@_ => sub {
  190. my ($ua, $tx) = @_;
  191. my $err = $tx->error;
  192. if (!$err || $err->{code}) { $promise->resolve($tx) }
  193. else { $promise->reject($err->{message}) }
  194. });
  195. return $promise;
  196. }
  197. # Perform non-blocking operations sequentially
  198. get_p('https://mojolicious.org')->then(sub {
  199. my $mojo = shift;
  200. say $mojo->res->code;
  201. return get('https://metacpan.org');
  202. })->then(sub {
  203. my $cpan = shift;
  204. say $cpan->res->code;
  205. })->catch(sub {
  206. my $err = shift;
  207. warn "Something went wrong: $err";
  208. })->wait;
  209. # Synchronize non-blocking operations (all)
  210. my $mojo = get_p('https://mojolicious.org');
  211. my $cpan = get_p('https://metacpan.org');
  212. Mojo::Promise->all($mojo, $cpan)->then(sub {
  213. my ($mojo, $cpan) = @_;
  214. say $mojo->[0]->res->code;
  215. say $cpan->[0]->res->code;
  216. })->catch(sub {
  217. my $err = shift;
  218. warn "Something went wrong: $err";
  219. })->wait;
  220. # Synchronize non-blocking operations (race)
  221. my $mojo = get_p('https://mojolicious.org');
  222. my $cpan = get_p('https://metacpan.org');
  223. Mojo::Promise->race($mojo, $cpan)->then(sub {
  224. my $tx = shift;
  225. say $tx->req->url, ' won!';
  226. })->catch(sub {
  227. my $err = shift;
  228. warn "Something went wrong: $err";
  229. })->wait;
  230. =head1 DESCRIPTION
  231. L<Mojo::Promise> is a Perl-ish implementation of
  232. L<Promises/A+|https://promisesaplus.com> and a superset of
  233. L<ES6 Promises|https://duckduckgo.com/?q=\mdn%20Promise>.
  234. =head1 STATES
  235. A promise is an object representing the eventual completion or failure of a
  236. non-blocking operation. It allows non-blocking functions to return values, like
  237. blocking functions. But instead of immediately returning the final value, the
  238. non-blocking function returns a promise to supply the value at some point in the
  239. future.
  240. A promise can be in one of three states:
  241. =over 2
  242. =item pending
  243. Initial state, neither fulfilled nor rejected.
  244. =item fulfilled
  245. Meaning that the operation completed successfully.
  246. =item rejected
  247. Meaning that the operation failed.
  248. =back
  249. A pending promise can either be fulfilled with a value or rejected with a
  250. reason. When either happens, the associated handlers queued up by a promise's
  251. L</"then"> method are called.
  252. =head1 ATTRIBUTES
  253. L<Mojo::Promise> implements the following attributes.
  254. =head2 ioloop
  255. my $loop = $promise->ioloop;
  256. $promise = $promise->ioloop(Mojo::IOLoop->new);
  257. Event loop object to control, defaults to the global L<Mojo::IOLoop> singleton.
  258. Note that this attribute is weakened.
  259. =head1 METHODS
  260. L<Mojo::Promise> inherits all methods from L<Mojo::Base> and implements
  261. the following new ones.
  262. =head2 all
  263. my $new = Mojo::Promise->all(@promises);
  264. Returns a new L<Mojo::Promise> object that either fulfills when all of the
  265. passed L<Mojo::Promise> objects have fulfilled or rejects as soon as one of them
  266. rejects. If the returned promise fulfills, it is fulfilled with the values from
  267. the fulfilled promises in the same order as the passed promises.
  268. =head2 all_settled
  269. my $new = Mojo::Promise->all_settled(@promises);
  270. Returns a new L<Mojo::Promise> object that fulfills when all of the passed
  271. L<Mojo::Promise> objects have fulfilled or rejected, with hash references that
  272. describe the outcome of each promise. Note that this method is B<EXPERIMENTAL>
  273. and might change without warning!
  274. =head2 any
  275. my $new = Mojo::Promise->any(@promises);
  276. Returns a new L<Mojo::Promise> object that fulfills as soon as one of
  277. the passed L<Mojo::Promise> objects fulfills, with the value from that promise.
  278. If no promises fulfill, it is rejected with the reasons from the rejected
  279. promises in the same order as the passed promises. Note that this method is
  280. B<EXPERIMENTAL> and might change without warning!
  281. =head2 catch
  282. my $new = $promise->catch(sub {...});
  283. Appends a rejection handler callback to the promise, and returns a new
  284. L<Mojo::Promise> object resolving to the return value of the callback if it is
  285. called, or to its original fulfillment value if the promise is instead
  286. fulfilled.
  287. # Longer version
  288. my $new = $promise->then(undef, sub {...});
  289. # Pass along the rejection reason
  290. $promise->catch(sub {
  291. my @reason = @_;
  292. warn "Something went wrong: $reason[0]";
  293. return @reason;
  294. });
  295. # Change the rejection reason
  296. $promise->catch(sub {
  297. my @reason = @_;
  298. return "This is bad: $reason[0]";
  299. });
  300. =head2 clone
  301. my $new = $promise->clone;
  302. Return a new L<Mojo::Promise> object cloned from this promise that is still
  303. pending.
  304. =head2 finally
  305. my $new = $promise->finally(sub {...});
  306. Appends a fulfillment and rejection handler to the promise, and returns a new
  307. L<Mojo::Promise> object resolving to the original fulfillment value or rejection
  308. reason.
  309. # Do something on fulfillment and rejection
  310. $promise->finally(sub {
  311. say "We are done!";
  312. });
  313. =head2 map
  314. my $new = Mojo::Promise->map(sub {...}, @items);
  315. my $new = Mojo::Promise->map({concurrency => 3}, sub {...}, @items);
  316. Apply a function that returns a L<Mojo::Promise> to each item in a list of
  317. items while optionally limiting concurrency. Returns a L<Mojo::Promise> that
  318. collects the results in the same manner as L</all>. If any item's promise is
  319. rejected, any remaining items which have not yet been mapped will not be. Note
  320. that this method is B<EXPERIMENTAL> and might change without warning!
  321. # Perform 3 requests at a time concurrently
  322. Mojo::Promise->map({concurrency => 3}, sub { $ua->get_p($_) }, @urls)
  323. ->then(sub{ say $_->[0]->res->dom->at('title')->text for @_ });
  324. These options are currently available:
  325. =over 2
  326. =item concurrency
  327. concurrency => 3
  328. The maximum number of items that are in progress at the same time.
  329. =back
  330. =head2 new
  331. my $promise = Mojo::Promise->new;
  332. my $promise = Mojo::Promise->new(sub {...});
  333. Construct a new L<Mojo::Promise> object.
  334. # Wrap a continuation-passing style API
  335. my $promise = Mojo::Promise->new(sub {
  336. my ($resolve, $reject) = @_;
  337. Mojo::IOLoop->timer(5 => sub {
  338. if (int rand 2) { $resolve->('Lucky!') }
  339. else { $reject->('Unlucky!') }
  340. });
  341. });
  342. =head2 race
  343. my $new = Mojo::Promise->race(@promises);
  344. Returns a new L<Mojo::Promise> object that fulfills or rejects as soon as one of
  345. the passed L<Mojo::Promise> objects fulfills or rejects, with the value or
  346. reason from that promise.
  347. =head2 reject
  348. my $new = Mojo::Promise->reject(@reason);
  349. $promise = $promise->reject(@reason);
  350. Build rejected L<Mojo::Promise> object or reject the promise with one or more
  351. rejection reasons.
  352. # Longer version
  353. my $promise = Mojo::Promise->new->reject(@reason);
  354. =head2 resolve
  355. my $new = Mojo::Promise->resolve(@value);
  356. $promise = $promise->resolve(@value);
  357. Build resolved L<Mojo::Promise> object or resolve the promise with one or more
  358. fulfillment values.
  359. # Longer version
  360. my $promise = Mojo::Promise->new->resolve(@value);
  361. =head2 then
  362. my $new = $promise->then(sub {...});
  363. my $new = $promise->then(sub {...}, sub {...});
  364. my $new = $promise->then(undef, sub {...});
  365. Appends fulfillment and rejection handlers to the promise, and returns a new
  366. L<Mojo::Promise> object resolving to the return value of the called handler.
  367. # Pass along the fulfillment value or rejection reason
  368. $promise->then(
  369. sub {
  370. my @value = @_;
  371. say "The result is $value[0]";
  372. return @value;
  373. },
  374. sub {
  375. my @reason = @_;
  376. warn "Something went wrong: $reason[0]";
  377. return @reason;
  378. }
  379. );
  380. # Change the fulfillment value or rejection reason
  381. $promise->then(
  382. sub {
  383. my @value = @_;
  384. return "This is good: $value[0]";
  385. },
  386. sub {
  387. my @reason = @_;
  388. return "This is bad: $reason[0]";
  389. }
  390. );
  391. =head2 timer
  392. my $new = Mojo::Promise->timer(5 => 'Success!');
  393. $promise = $promise->timer(5 => 'Success!');
  394. $promise = $promise->timer(5);
  395. Create a new L<Mojo::Promise> object with a timer or attach a timer to an
  396. existing promise. The promise will be resolved after the given amount of time in
  397. seconds with or without a value. Note that this method is B<EXPERIMENTAL> and
  398. might change without warning!
  399. =head2 timeout
  400. my $new = Mojo::Promise->timeout(5 => 'Timeout!');
  401. $promise = $promise->timeout(5 => 'Timeout!');
  402. $promise = $promise->timeout(5);
  403. Create a new L<Mojo::Promise> object with a timeout or attach a timeout to an
  404. existing promise. The promise will be rejected after the given amount of time in
  405. seconds with a reason, which defaults to C<Promise timeout>. Note that this
  406. method is B<EXPERIMENTAL> and might change without warning!
  407. =head2 wait
  408. $promise->wait;
  409. Start L</"ioloop"> and stop it again once the promise has been fulfilled or
  410. rejected, does nothing when L</"ioloop"> is already running.
  411. =head1 SEE ALSO
  412. L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
  413. =cut