Unified
Split
Showing
with
465 additions
and 45 deletions.
- +4 −0 Changes
- +2 −2 README.md
- +25 −1 lib/Mojo/Base.pm
- +106 −30 lib/Mojo/Promise.pm
- +13 −2 lib/Mojolicious.pm
- +9 −7 lib/Mojolicious/Command/version.pm
- +108 −0 lib/Mojolicious/Guides/Cookbook.pod
- +65 −2 t/mojo/promise.t
- +125 −0 t/mojo/promise_async_await.t
- +8 −1 t/pod_coverage.t
| @@ -1,4 +1,8 @@ | ||
|
|
||
| 8.28 2019-12-26 | ||
| - Added EXPERIMENTAL support for async/await (with -async Mojo::Base flag). | ||
| - Added EXPERIMENTAL all_settled and any methods to Mojo::Promise. | ||
|
|
||
| 8.27 2019-12-04 | ||
| - Added EXPERIMENTAL before_command hook. | ||
| - Added EXPERIMENTAL scope_guard function to Mojo::Util. | ||
| @@ -34,8 +34,8 @@ | ||
| applications, independently of the web framework. | ||
| * Full stack HTTP and WebSocket client/server implementation with IPv6, TLS, | ||
| SNI, IDNA, HTTP/SOCKS5 proxy, UNIX domain socket, Comet (long polling), | ||
| Promises/A+, keep-alive, connection pooling, timeout, cookie, multipart, | ||
| and gzip compression support. | ||
| Promises/A+, async/await, keep-alive, connection pooling, timeout, cookie, | ||
| multipart, and gzip compression support. | ||
| * Built-in non-blocking I/O web server, supporting multiple event loops as | ||
| well as optional pre-forking and hot deployment, perfect for building | ||
| highly scalable web services. | ||
| @@ -20,6 +20,13 @@ use IO::Handle (); | ||
| use constant ROLES => | ||
| !!(eval { require Role::Tiny; Role::Tiny->VERSION('2.000001'); 1 }); | ||
|
|
||
| # async/await support requires Future::AsyncAwait::Frozen 0.36+ | ||
| use constant ASYNC => $ENV{MOJO_NO_ASYNC} ? 0 : !!(eval { | ||
| require Future::AsyncAwait::Frozen; | ||
| Future::AsyncAwait::Frozen->VERSION('0.000001'); | ||
| 1; | ||
| }); | ||
|
|
||
| # Protect subclasses using AUTOLOAD | ||
| sub DESTROY { } | ||
|
|
||
| @@ -120,6 +127,14 @@ sub import { | ||
| eval "package $caller; use Role::Tiny; 1" or die $@; | ||
| } | ||
|
|
||
| # async/await | ||
| elsif ($flag eq '-async') { | ||
| Carp::croak 'Future::AsyncAwait::Frozen 0.36+ is required for async/await' | ||
| unless ASYNC; | ||
| Future::AsyncAwait::Frozen->import_into($caller, | ||
| future_class => 'Mojo::Promise'); | ||
| } | ||
|
|
||
| # Signatures (Perl 5.20+) | ||
| elsif ($flag eq '-signatures') { | ||
| Carp::croak 'Subroutine signatures require Perl 5.20+' if $] < 5.020; | ||
| @@ -257,14 +272,23 @@ enable support for L<subroutine signatures|perlsub/"Signatures">. | ||
| use Mojo::Base 'SomeBaseClass', -signatures; | ||
| use Mojo::Base -role, -signatures; | ||
| If you have L<Future::AsyncAwait::Frozen> 0.36+ installed you can also use the | ||
| C<-async> flag to activate the C<async> and C<await> keywords to deal much more | ||
| efficiently with promises. Note that this feature is B<EXPERIMENTAL> and might | ||
| change without warning! | ||
| # Also enable async/await | ||
| use Mojo::Base -strict, -async; | ||
| use Mojo::Base -base, -signatures, -async; | ||
| This will also disable experimental warnings on versions of Perl where this | ||
| feature was still experimental. | ||
| =head1 FLUENT INTERFACES | ||
| Fluent interfaces are a way to design object-oriented APIs around method | ||
| chaining to create domain-specific languages, with the goal of making the | ||
| readablity of the source code close to written prose. | ||
| readability of the source code close to written prose. | ||
| package Duck; | ||
| use Mojo::Base -base; | ||
| @@ -7,25 +7,35 @@ use Scalar::Util 'blessed'; | ||
|
|
||
| has ioloop => sub { Mojo::IOLoop->singleton }, weak => 1; | ||
|
|
||
| sub all { | ||
| my ($class, @promises) = @_; | ||
| sub AWAIT_CLONE { shift->clone } | ||
|
|
||
| my $all = $promises[0]->clone; | ||
| my $results = []; | ||
| my $remaining = scalar @promises; | ||
| for my $i (0 .. $#promises) { | ||
| $promises[$i]->then( | ||
| sub { | ||
| $results->[$i] = [@_]; | ||
| $all->resolve(@$results) if --$remaining <= 0; | ||
| }, | ||
| sub { $all->reject(@_) } | ||
| ); | ||
| } | ||
| sub AWAIT_DONE { shift->resolve(@_) } | ||
| sub AWAIT_FAIL { shift->reject(@_) } | ||
|
|
||
| return $all; | ||
| sub AWAIT_GET { | ||
| my $self = shift; | ||
| my @results = @{$self->{result} // []}; | ||
| die $results[0] unless $self->{status} eq 'resolve'; | ||
| return wantarray ? @results : $results[0]; | ||
| } | ||
|
|
||
| sub AWAIT_IS_CANCELLED {undef} | ||
|
|
||
| sub AWAIT_IS_READY { | ||
| my $self = shift; | ||
| return !!$self->{result} && !@{$self->{resolve}} && !@{$self->{reject}}; | ||
| } | ||
|
|
||
| sub AWAIT_NEW_DONE { shift->resolve(@_) } | ||
| sub AWAIT_NEW_FAIL { shift->reject(@_) } | ||
|
|
||
| sub AWAIT_ON_CANCEL { } | ||
| sub AWAIT_ON_READY { shift->finally(@_) } | ||
|
|
||
| sub all { _all(2, @_) } | ||
| sub all_settled { _all(0, @_) } | ||
| sub any { _all(3, @_) } | ||
|
|
||
| sub catch { shift->then(undef, shift) } | ||
|
|
||
| sub clone { $_[0]->new->ioloop($_[0]->ioloop) } | ||
| @@ -44,7 +54,7 @@ sub finally { | ||
|
|
||
| sub map { | ||
| my ($class, $options) = (shift, ref $_[0] eq 'HASH' ? shift : {}); | ||
| my ($cb, @items) = @_; | ||
| my ($cb, @items) = @_; | ||
|
|
||
| my @start = map { $_->$cb } splice @items, 0, | ||
| $options->{concurrency} // @items; | ||
| @@ -80,12 +90,7 @@ sub new { | ||
| return $self; | ||
| } | ||
|
|
||
| sub race { | ||
| my ($class, @promises) = @_; | ||
| my $new = $promises[0]->clone; | ||
| $_->then(sub { $new->resolve(@_) }, sub { $new->reject(@_) }) for @promises; | ||
| return $new; | ||
| } | ||
| sub race { _all(1, @_) } | ||
|
|
||
| sub reject { shift->_settle('reject', @_) } | ||
| sub resolve { shift->_settle('resolve', @_) } | ||
| @@ -113,6 +118,59 @@ sub wait { | ||
| $loop->start until $done; | ||
| } | ||
|
|
||
| sub _all { | ||
| my ($type, $class, @promises) = @_; | ||
|
|
||
| my $all = $promises[0]->clone; | ||
| my $results = []; | ||
| my $remaining = scalar @promises; | ||
| for my $i (0 .. $#promises) { | ||
|
|
||
| # "race" | ||
| if ($type == 1) { | ||
| $promises[$i]->then(sub { $all->resolve(@_) }, sub { $all->reject(@_) }); | ||
| } | ||
|
|
||
| # "all" | ||
| elsif ($type == 2) { | ||
| $promises[$i]->then( | ||
| sub { | ||
| $results->[$i] = [@_]; | ||
| $all->resolve(@$results) if --$remaining <= 0; | ||
| }, | ||
| sub { $all->reject(@_) } | ||
| ); | ||
| } | ||
|
|
||
| # "any" | ||
| elsif ($type == 3) { | ||
| $promises[$i]->then( | ||
| sub { $all->resolve(@_) }, | ||
| sub { | ||
| $results->[$i] = [@_]; | ||
| $all->reject(@$results) if --$remaining <= 0; | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| # "all_settled" | ||
| else { | ||
| $promises[$i]->then( | ||
| sub { | ||
| $results->[$i] = {status => 'fulfilled', value => [@_]}; | ||
| $all->resolve(@$results) if --$remaining <= 0; | ||
| }, | ||
| sub { | ||
| $results->[$i] = {status => 'rejected', reason => [@_]}; | ||
| $all->resolve(@$results) if --$remaining <= 0; | ||
| } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return $all; | ||
| } | ||
|
|
||
| sub _defer { | ||
| my $self = shift; | ||
|
|
||
| @@ -178,7 +236,7 @@ Mojo::Promise - Promises/A+ | ||
| # Wrap continuation-passing style APIs with promises | ||
| my $ua = Mojo::UserAgent->new; | ||
| sub get { | ||
| sub get_p { | ||
| my $promise = Mojo::Promise->new; | ||
| $ua->get(@_ => sub { | ||
| my ($ua, $tx) = @_; | ||
| @@ -190,7 +248,7 @@ Mojo::Promise - Promises/A+ | ||
| } | ||
| # Perform non-blocking operations sequentially | ||
| get('https://mojolicious.org')->then(sub { | ||
| get_p('https://mojolicious.org')->then(sub { | ||
| my $mojo = shift; | ||
| say $mojo->res->code; | ||
| return get('https://metacpan.org'); | ||
| @@ -203,8 +261,8 @@ Mojo::Promise - Promises/A+ | ||
| })->wait; | ||
| # Synchronize non-blocking operations (all) | ||
| my $mojo = get('https://mojolicious.org'); | ||
| my $cpan = get('https://metacpan.org'); | ||
| my $mojo = get_p('https://mojolicious.org'); | ||
| my $cpan = get_p('https://metacpan.org'); | ||
| Mojo::Promise->all($mojo, $cpan)->then(sub { | ||
| my ($mojo, $cpan) = @_; | ||
| say $mojo->[0]->res->code; | ||
| @@ -215,8 +273,8 @@ Mojo::Promise - Promises/A+ | ||
| })->wait; | ||
| # Synchronize non-blocking operations (race) | ||
| my $mojo = get('https://mojolicious.org'); | ||
| my $cpan = get('https://metacpan.org'); | ||
| my $mojo = get_p('https://mojolicious.org'); | ||
| my $cpan = get_p('https://metacpan.org'); | ||
| Mojo::Promise->race($mojo, $cpan)->then(sub { | ||
| my $tx = shift; | ||
| say $tx->req->url, ' won!'; | ||
| @@ -285,8 +343,26 @@ the following new ones. | ||
| Returns a new L<Mojo::Promise> object that either fulfills when all of the | ||
| passed L<Mojo::Promise> objects have fulfilled or rejects as soon as one of them | ||
| rejects. If the returned promise fulfills, it is fulfilled with the values from | ||
| the fulfilled promises in the same order as the passed promises. This method can | ||
| be useful for aggregating results of multiple promises. | ||
| the fulfilled promises in the same order as the passed promises. | ||
| =head2 all_settled | ||
| my $new = Mojo::Promise->all_settled(@promises); | ||
| Returns a new L<Mojo::Promise> object that fulfills when all of the passed | ||
| L<Mojo::Promise> objects have fulfilled or rejected, with hash references that | ||
| describe the outcome of each promise. Note that this method is B<EXPERIMENTAL> | ||
| and might change without warning! | ||
| =head2 any | ||
| my $new = Mojo::Promise->any(@promises); | ||
| Returns a new L<Mojo::Promise> object that fulfills as soon as one of | ||
| the passed L<Mojo::Promise> objects fulfills, with the value from that promise. | ||
| If no promises fulfill, it is rejected with the reasons from the rejected | ||
| promises in the same order as the passed promises. Note that this method is | ||
| B<EXPERIMENTAL> and might change without warning! | ||
| =head2 catch | ||
| @@ -59,7 +59,7 @@ has ua => sub { Mojo::UserAgent->new }; | ||
| has validator => sub { Mojolicious::Validator->new }; | ||
|
|
||
| our $CODENAME = 'Supervillain'; | ||
| our $VERSION = '8.27'; | ||
| our $VERSION = '8.28'; | ||
|
|
||
| sub BUILD_DYNAMIC { | ||
| my ($class, $method, $dyn_methods) = @_; | ||
| @@ -137,7 +137,7 @@ sub handler { | ||
|
|
||
| # Dispatcher has to be last in the chain | ||
| ++$self->{dispatch} | ||
| and $self->hook(around_action => sub { $_[2]($_[1]) }) | ||
| and $self->hook(around_action => \&_action) | ||
| and $self->hook(around_dispatch => sub { $_[1]->app->dispatch($_[1]) }) | ||
| unless $self->{dispatch}; | ||
|
|
||
| @@ -197,6 +197,17 @@ sub start { | ||
|
|
||
| sub startup { } | ||
|
|
||
| sub _action { | ||
| my ($next, $c, $action, $last) = @_; | ||
|
|
||
| my $val = $action->($c); | ||
| $val->catch(sub { $c->helpers->reply->exception(shift) }) | ||
| ->finally(sub { undef $val }) | ||
| if Scalar::Util::blessed $val && $val->isa('Mojo::Promise'); | ||
|
|
||
| return $val; | ||
| } | ||
|
|
||
| sub _die { CORE::die ref $_[0] ? $_[0] : Mojo::Exception->new(shift)->trace } | ||
|
|
||
| sub _exception { | ||
| @@ -18,20 +18,22 @@ sub run { | ||
| = Mojo::IOLoop::Client->can_socks ? $IO::Socket::Socks::VERSION : 'n/a'; | ||
| my $tls = Mojo::IOLoop::TLS->can_tls ? $IO::Socket::SSL::VERSION : 'n/a'; | ||
| my $nnr = Mojo::IOLoop::Client->can_nnr ? $Net::DNS::Native::VERSION : 'n/a'; | ||
| my $roles = Mojo::Base->ROLES ? $Role::Tiny::VERSION : 'n/a'; | ||
| my $roles = Mojo::Base->ROLES ? $Role::Tiny::VERSION : 'n/a'; | ||
| my $async = Mojo::Base->ASYNC ? $Future::AsyncAwait::Frozen::VERSION : 'n/a'; | ||
|
|
||
| print <<EOF; | ||
| CORE | ||
| Perl ($^V, $^O) | ||
| Mojolicious ($Mojolicious::VERSION, $Mojolicious::CODENAME) | ||
| OPTIONAL | ||
| Cpanel::JSON::XS 4.09+ ($json) | ||
| EV 4.0+ ($ev) | ||
| IO::Socket::Socks 0.64+ ($socks) | ||
| IO::Socket::SSL 2.009+ ($tls) | ||
| Net::DNS::Native 0.15+ ($nnr) | ||
| Role::Tiny 2.000001+ ($roles) | ||
| Cpanel::JSON::XS 4.09+ ($json) | ||
| EV 4.0+ ($ev) | ||
| IO::Socket::Socks 0.64+ ($socks) | ||
| IO::Socket::SSL 2.009+ ($tls) | ||
| Net::DNS::Native 0.15+ ($nnr) | ||
| Role::Tiny 2.000001+ ($roles) | ||
| Future::AsyncAwait::Frozen 0.36+ ($async) | ||
| EOF | ||
|
|
||
Oops, something went wrong.