Skip to content

Instantly share code, notes, and snippets.

/thenable.diff Secret

Created October 22, 2017 11:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/7fb9eb394fc406da2ae26f6acd079583 to your computer and use it in GitHub Desktop.
Save anonymous/7fb9eb394fc406da2ae26f6acd079583 to your computer and use it in GitHub Desktop.
diff --git a/lib/Mojo/IOLoop.pm b/lib/Mojo/IOLoop.pm
index 5b54d51ba..0db6afda5 100644
--- a/lib/Mojo/IOLoop.pm
+++ b/lib/Mojo/IOLoop.pm
@@ -406,10 +406,8 @@ takes the same arguments as L<Mojo::IOLoop::Client/"connect">.
my $delay = $loop->delay(sub {...});
my $delay = $loop->delay(sub {...}, sub {...});
-Build L<Mojo::IOLoop::Delay> object to manage callbacks and control the flow of
-events for this event loop, which can help you avoid deep nested closures that
-often result from continuation-passing style. Callbacks will be passed along to
-L<Mojo::IOLoop::Delay/"steps">.
+Build L<Mojo::IOLoop::Delay> object to use as a promise or for flow-control.
+Callbacks will be passed along to L<Mojo::IOLoop::Delay/"steps">.
# Synchronize multiple non-blocking operations
my $delay = Mojo::IOLoop->delay(sub { say 'BOOM!' });
diff --git a/lib/Mojo/IOLoop/Delay.pm b/lib/Mojo/IOLoop/Delay.pm
index b918b1fcf..9091aa848 100644
--- a/lib/Mojo/IOLoop/Delay.pm
+++ b/lib/Mojo/IOLoop/Delay.pm
@@ -3,6 +3,7 @@ use Mojo::Base 'Mojo::EventEmitter';
use Mojo::IOLoop;
use Mojo::Util;
+use Scalar::Util 'weaken';
has ioloop => sub { Mojo::IOLoop->singleton };
has remaining => sub { [] };
@@ -18,12 +19,39 @@ sub data { Mojo::Util::_stash(data => @_) }
sub pass { $_[0]->begin->(@_) }
+sub race {
+ my $self = shift;
+ my $next = $self->_clone;
+ $_->then(sub { $next->resolve(@_) }, sub { $next->reject(@_) }) for $self, @_;
+ return $next;
+}
+
+sub reject { shift->_finish(error => @_) }
+sub resolve { shift->_finish(finish => @_) }
+
sub steps {
my $self = shift->remaining([@_]);
$self->ioloop->next_tick($self->begin);
return $self;
}
+sub then {
+ my ($self, $finish, $error) = @_;
+
+ my $next = $self->_clone;
+ $self->once(finish => sub { shift; $next->resolve(@_) });
+ $self->once(error => sub { shift; $next->reject(@_) });
+ $next->once(finish => sub { shift; $finish->(@_) }) if $finish;
+ $next->once(error => sub { shift; $error->(@_) }) if $error;
+
+ return $next unless $self->{settled};
+
+ my $method = $self->{error} ? 'reject' : 'resolve';
+ my $args = $self->{error} || $self->{finish};
+ $next->ioloop->next_tick(sub { $next->$method(@$args) });
+ return $next;
+}
+
sub wait {
my $self = shift;
return if $self->ioloop->is_running;
@@ -32,24 +60,37 @@ sub wait {
$self->ioloop->start;
}
+sub _clone {
+ my $self = shift;
+ my $clone = $self->new;
+ weaken $clone->ioloop($self->ioloop)->{ioloop};
+ return $clone;
+}
+
sub _die { $_[0]->has_subscribers('error') ? $_[0]->ioloop->stop : die $_[1] }
+sub _finish {
+ my ($self, $event) = (shift, shift);
+ $self->{settled} ? return $self : $self->{settled}++;
+ $self->{$event} = [@_];
+ return $self->remaining([])->emit($event => @_);
+}
+
sub _step {
my ($self, $id, $offset, $len) = (shift, shift, shift, shift);
$self->{args}[$id]
= [@_ ? defined $len ? splice @_, $offset, $len : splice @_, $offset : ()];
- return $self if $self->{fail} || --$self->{pending} || $self->{lock};
+ return $self if $self->{settled} || --$self->{pending} || $self->{lock};
local $self->{lock} = 1;
my @args = map {@$_} @{delete $self->{args}};
$self->{counter} = 0;
if (my $cb = shift @{$self->remaining}) {
- eval { $self->$cb(@args); 1 }
- or (++$self->{fail} and return $self->remaining([])->emit(error => $@));
+ eval { $self->$cb(@args); 1 } or $self->reject($@);
}
- return $self->remaining([])->emit(finish => @args) unless $self->{counter};
+ return $self->resolve(@args) unless $self->{counter};
$self->ioloop->next_tick($self->begin) unless $self->{pending};
return $self;
}
@@ -60,7 +101,7 @@ sub _step {
=head1 NAME
-Mojo::IOLoop::Delay - Manage callbacks and control the flow of events
+Mojo::IOLoop::Delay - Promises/A+ and flow-control helpers
=head1 SYNOPSIS
@@ -120,9 +161,10 @@ Mojo::IOLoop::Delay - Manage callbacks and control the flow of events
=head1 DESCRIPTION
-L<Mojo::IOLoop::Delay> manages callbacks and controls the flow of events for
-L<Mojo::IOLoop>, which can help you avoid deep nested closures that often
-result from continuation-passing style.
+L<Mojo::IOLoop::Delay> is a Perl-ish implementation of
+L<Promises/A+|https://promisesaplus.com/> and provides flow-control helpers for
+L<Mojo::IOLoop>, which can help you avoid deep nested closures that often result
+from continuation-passing style.
use Mojo::IOLoop;
@@ -304,6 +346,18 @@ next step.
# Longer version
$delay->begin(0)->(@args);
+=head2 race
+
+ my $thenable = $delay->race(@thenables);
+
+=head2 reject
+
+ $delay = $delay->reject(@args);
+
+=head2 resolve
+
+ $delay = $delay->resolve(@args);
+
=head2 steps
$delay = $delay->steps(sub {...}, sub {...});
@@ -314,6 +368,10 @@ unless it is delayed by incrementing the event counter. This chain will continue
until there are no L</"remaining"> callbacks, a callback does not increment the
event counter or an exception gets thrown in a callback.
+=head2 then
+
+ my $thenable = $delay->then(sub {...}, sub {...});
+
=head2 wait
$delay->wait;
diff --git a/t/mojo/delay.t b/t/mojo/delay.t
index 35e51d1a5..0dc5d9f29 100644
--- a/t/mojo/delay.t
+++ b/t/mojo/delay.t
@@ -20,6 +20,61 @@ $end2->();
$delay->wait;
is_deeply \@results, [1, 1], 'right results';
+# Thenable
+my ($resolve, $reject, $resolve2, $reject2);
+$delay = Mojo::IOLoop::Delay->new;
+my $delay2 = $delay->then(sub { $resolve = shift }, sub { $reject = shift });
+$delay2->then(sub { $resolve2 = shift }, sub { $reject2 = shift });
+$delay->resolve('works');
+is $resolve, 'works', 'promise was resolved';
+is $resolve2, 'works', 'promise was resolved';
+is $reject, undef, 'promise was not rejected';
+is $reject2, undef, 'promise was not rejected';
+
+# Thenable (race)
+($resolve, $reject) = ();
+my $promise = Mojo::IOLoop::Delay->new;
+my $promise2 = Mojo::IOLoop::Delay->new;
+my $promise3 = Mojo::IOLoop::Delay->new;
+$promise->race($promise2, $promise3)
+ ->then(sub { $resolve = shift }, sub { $reject = shift });
+$promise->resolve('first');
+$promise2->resolve('second');
+$promise3->resolve('third');
+is $resolve, 'first', 'promise was resolved';
+is $reject, undef, 'promise was not rejected';
+($resolve, $reject) = ();
+$promise = Mojo::IOLoop::Delay->new;
+$promise2 = Mojo::IOLoop::Delay->new;
+$promise3 = Mojo::IOLoop::Delay->new;
+$promise->race($promise2, $promise3)
+ ->then(sub { $resolve = shift }, sub { $reject = shift });
+$promise3->reject('third');
+$promise->reject('first');
+$promise2->reject('second');
+is $resolve, undef, 'promise was not resolved';
+is $reject, 'third', 'promise was rejected';
+
+# Thenable (already settled)
+($resolve, $reject) = ();
+$delay = Mojo::IOLoop::Delay->new;
+$delay->resolve('works');
+$delay->then(sub { $resolve = shift }, sub { $reject = shift });
+is $resolve, undef, 'no value';
+is $reject, undef, 'no value';
+$delay->ioloop->one_tick;
+is $resolve, 'works', 'right value';
+is $reject, undef, 'no value';
+($resolve, $reject) = ();
+$delay = Mojo::IOLoop::Delay->new->catch(sub { });
+$delay->reject('works too');
+$delay->then(sub { $resolve = shift }, sub { $reject = shift });
+is $resolve, undef, 'no value';
+is $reject, undef, 'no value';
+$delay->ioloop->one_tick;
+is $resolve, undef, 'no value';
+is $reject, 'works too', 'right value';
+
# Argument splicing
$delay = Mojo::IOLoop::Delay->new;
Mojo::IOLoop->next_tick($delay->begin);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment