Future-Q

 view release on metacpan or  search on metacpan

lib/Future/Q.pm  view on Meta::CPAN

package Future::Q;
use strict;
use warnings;
use Future 0.29;
use parent "Future";
use Devel::GlobalDestruction;
use Scalar::Util qw(refaddr blessed weaken);
use Carp;
use Try::Tiny ();

our $VERSION = '0.120';

our @CARP_NOT = qw(Try::Tiny Future Future::PP Future::XS);

our $OnError = undef;

## ** lexical attributes to avoid collision of names.

my %failure_handled_for = ();

sub new {
    my ($class, @args) = @_;
    my $self = $class->SUPER::new(@args);
    my $id = refaddr $self;
    $failure_handled_for{$id} = 0;
    return $self;
}

sub _q_go_super_DESTROY {
    my ($self) = @_;
    my $super_destroy = $self->can("SUPER::DESTROY");
    goto $super_destroy if defined $super_destroy;
}

sub DESTROY {
    my ($self) = @_;
    if(in_global_destruction) {
        goto \&_q_go_super_DESTROY;
    }
    my $id = refaddr $self;
    if($self->is_ready && $self->failure && !$failure_handled_for{$id}) {
        $self->_q_warn_failure();
        my @failed_subfutures = Try::Tiny::try {
            $self->failed_futures;
        }Try::Tiny::catch {
            ();
        };
        foreach my $f (@failed_subfutures) {
            $f->_q_warn_failure(is_subfuture => 1) if blessed($f) && $f->can('_q_warn_failure');
        }
    }
    delete $failure_handled_for{$id};
    goto \&_q_go_super_DESTROY;
}

sub _q_set_failure_handled {
    my ($self) = @_;
    $failure_handled_for{refaddr $self} = 1;
}

sub _q_warn_failure {
    my ($self, %options) = @_;
    if($self->is_ready && $self->failure) {
        my $failure = $self->failure;
        my $message = Carp::shortmess($options{is_subfuture}
                                      ? "Failure of subfuture $self may not be handled: $failure  subfuture may be lost"
                                      : "Failure of $self is not handled: $failure  future is lost");
        if(defined($OnError) && ref($OnError) eq "CODE") {
            $OnError->($message);
        }else {
            warn $message;
        }
    }
}

sub try {
    my ($class, $func, @args) = @_;
    if(!defined($func) || ref($func) ne "CODE") {
        $func = sub {
            croak("func parameter for try() must be a code-ref");
        };
    }
    my $result_future = Try::Tiny::try {
        my @results = $func->(@args);
        if(scalar(@results) == 1 && blessed($results[0]) && $results[0]->isa('Future')) {
            return $results[0];
        }else {
            return $class->new->fulfill(@results);
        }
    } Try::Tiny::catch {
        my $e = shift;
        return $class->new->reject($e);
    };
    return $result_future;
}

sub fcall {
    goto $_[0]->can('try');
}

sub then {
    my ($self, $on_fulfilled, $on_rejected) = @_;
    if(defined($on_fulfilled) && ref($on_fulfilled) ne "CODE") {
        $on_fulfilled = undef;
    }
    if(defined($on_rejected) && ref($on_rejected) ne "CODE") {
        $on_rejected = undef;
    }
    my $class = ref($self);
    $self->_q_set_failure_handled();
    
    my $next_future = $self->new;
    $self->on_ready(sub {
        my $invo_future = shift;
        if($invo_future->is_cancelled) {
            $next_future->cancel() if $next_future->is_pending;
            return;
        }
        my $return_future = $invo_future;
        if($invo_future->is_rejected && defined($on_rejected)) {
            $return_future = $class->try($on_rejected, $invo_future->failure);
        }elsif($invo_future->is_fulfilled && defined($on_fulfilled)) {
            $return_future = $class->try($on_fulfilled, $invo_future->get);
        }
        $next_future->resolve($return_future);
    });
    if($next_future->is_pending && $self->is_pending) {
        weaken(my $invo_future = $self);
        $next_future->on_cancel(sub {
            if(defined($invo_future) && $invo_future->is_pending) {
                $invo_future->cancel();
            }
        });
    }
    return $next_future;
}

sub catch {
    my ($self, $on_rejected) = @_;
    @_ = ($self, undef, $on_rejected);
    goto $self->can('then');
}

sub fulfill {
    goto $_[0]->can('done');
}

sub resolve {
    my ($self, @result) = @_;
    if(not (@result == 1 && blessed($result[0]) && $result[0]->isa("Future"))) {
        goto $self->can("fulfill");
    }
    return $self if $self->is_cancelled;
    my $base_future = $result[0];

    ## Maybe we should check if $base_future is identical to
    ## $self. Promises/A+ spec v1.1 [1] states we should reject $self
    ## in that case. However, since Q v1.0.1 does not care that case,
    ## we also leave that case unchecked for now.
    ##
    ## [1]: https://github.com/promises-aplus/promises-spec/tree/1.1.0
    
    $base_future->on_ready(sub {
        my $base_future = shift;
        return if $self->is_ready;
        if($base_future->is_cancelled) {
            $self->cancel();
        }elsif($base_future->failure) {
            if($base_future->can("_q_set_failure_handled")) {
                $base_future->_q_set_failure_handled();
            }
            $self->reject($base_future->failure);
        }else {
            $self->fulfill($base_future->get);
        }
    });
    if(!$base_future->is_ready) {
        weaken(my $weak_base = $base_future);
        $self->on_cancel(sub {
            $weak_base->cancel() if defined($weak_base) && !$weak_base->is_ready;
        });
    }
    return $self;
}

sub reject {
    goto $_[0]->can('fail');
}

sub is_pending {
    my ($self) = @_;
    return !$self->is_ready;
}

sub is_fulfilled {
    my ($self) = @_;
    return (!$self->is_pending && !$self->is_cancelled && !$self->is_rejected);
}

sub is_rejected {
    my ($self) = @_;
    return ($self->is_ready && !!$self->failure);
}

foreach my $method (qw(wait_all wait_any needs_all needs_any)) {
    no strict "refs";
    my $supermethod_code = __PACKAGE__->can("SUPER::$method");
    *{$method} = sub {
        my ($self, @subfutures) = @_;
        foreach my $sub (@subfutures) {
            next if !blessed($sub) || !$sub->can('_q_set_failure_handled');
            $sub->_q_set_failure_handled();
        }
        goto $supermethod_code;
    };
}

sub finally {
    my ($self, $callback) = @_;
    my $class = ref($self);
    $self->_q_set_failure_handled();
    if(!defined($callback) || ref($callback) ne "CODE") {
        return $class->new->reject("Callback for finally() must be a code-ref");
    }
    my $next_future = $self->new;
    $self->on_ready(sub {
        my ($invo_future) = @_;
        if($invo_future->is_cancelled) {
            $next_future->cancel if $next_future->is_pending;
            return;
        }
        my $returned_future = $class->try($callback);
        $returned_future->on_ready(sub {
            my ($returned_future) = @_;
            if(!$returned_future->is_cancelled && $returned_future->failure) {
                $next_future->resolve($returned_future);
            }else {
                $next_future->resolve($invo_future);
            }
        });
        if(!$returned_future->is_ready) {
            weaken(my $weak_returned = $returned_future);
            $next_future->on_cancel(sub {
                $weak_returned->cancel if defined($weak_returned) && !$weak_returned->is_ready;
            });
        }
    });
    if(!$self->is_ready) {
        weaken(my $weak_invo = $self);
        $next_future->on_cancel(sub {
            $weak_invo->cancel if defined($weak_invo) && !$weak_invo->is_ready;
        
        });
    }
    return $next_future;
}

1;

__END__

=head1 NAME

Future::Q - a Future (or Promise or Deferred) like Q module for JavaScript

=head1 SYNOPSIS

    use Future::Q;

    sub async_func_future {
        my @args = @_;
        my $f = Future::Q->new;
        other_async_func(   ## This is a regular callback-style async function
            args => \@args,
            on_success => sub { $f->fulfill(@_) },
            on_failure => sub { $f->reject(@_) },
        );
        return $f;
    }

    async_func_future()->then(sub {
        my @results = @_;
        my @processed_values = do_some_processing(@results);
        return @processed_values;
    })->then(sub {
        my @values = @_;   ## same values as @processed_values
        return async_func_future(@values);
    })->then(sub {
        warn "Operation finished.\n";
    })->catch(sub {
        ## failure handler
        my $error = shift;
        warn "Error: $error\n";
    });

=head1 DESCRIPTION

L<Future::Q> is a subclass of L<Future>.
It extends its API with C<then()> and C<try()> etc, which are
almost completely compatible with Kris Kowal's Q module for JavaScript.

L<Future::Q>'s API and documentation is designed to be self-contained,
at least for basic usage of Futures.
If a certain function you want is missing in this module,
you should refer to L</Missing Methods and Features> section and/or L<Future>.
(But be prepared because L<Future> has a lot of methods!)

Basically a Future (in a broad meaning) represents an operation (whether it's in progress
or finished) and its results.



( run in 1.887 second using v1.01-cache-2.11-cpan-0bb4e1dffa6 )