Mojo-AsyncAwait

 view release on metacpan or  search on metacpan

lib/Mojo/AsyncAwait/Backend/Coro.pm  view on Meta::CPAN

package Mojo::AsyncAwait::Backend::Coro;
use Mojo::Base -strict;

use Carp ();
use Coro::State ();
use Mojo::Util;
use Mojo::Promise;
use Sub::Util ();

use Exporter 'import';

our @EXPORT = (qw/async await/);

my $main = Coro::State->new;
$main->{desc} = 'Mojo::AsyncAwait::Backend::Coro/$main';

# LIFO stack of coroutines waiting to come back to
# always has $main as the bottom of the stack
my @stack = ($main);

# Coroutines that are ostensible done but need someone to kill them
my @clean;

# _push adds a coroutine to the stack and enters it
# when control returns to the original pusher, it will clean up
# any coroutines that are waiting to be cleaned up

sub _push {
  push @stack, @_;
  $stack[-2]->transfer($stack[-1]);
  $_->cancel for @clean;
  @clean = ();
}

# _pop pops the current coroutine off the stack. If given a callback, it calls
# a callback on it, otherwise, schedules it for cleanup. It then transfers to
# the next one on the stack. Note that it can't pop-and-return (which would
# make more sense) because any action on it must happen before control is
# transfered away from it

sub _pop (;&) {
  Carp::croak "Cannot leave the main thread"
    if $stack[-1] == $main;
  my ($cb) = @_;
  my $current = pop @stack;
  if ($cb) { $cb->($current)       }
  else     { push @clean, $current }
  $current->transfer($stack[-1]);
}

sub async {
  my $body   = pop;
  my $opts   = _parse_opts(@_);
  my @caller = caller;

  my $subname  = "$caller[0]::__ASYNCSUB__";
  my $bodyname = "$caller[0]::__ASYNCBODY__";
  if (defined(my $name = $opts->{-name})) {
    $subname  = $opts->{-install} ? "$caller[0]::$name" : "$subname($name)";
    $bodyname .= "($name)";
  }
  my $desc = "declared at $caller[1] line $caller[2]";

  Sub::Util::set_subname($bodyname => $body)
    if Sub::Util::subname($body) =~ /::__ANON__$/;

  my $wrapped = sub {
    my @caller  = caller;
    my $promise = Mojo::Promise->new;
    my $coro    = Coro::State->new(sub {
      eval {
        BEGIN { $^H{'Mojo::AsyncAwait::Backend::Coro/async'} = 1 }
        $promise->resolve($body->(@_)); 1
      } or $promise->reject($@);
      _pop;
    }, @_);
    $coro->{desc} = "$subname called at $caller[1] line $caller[2], $desc";
    _push $coro;
    return $promise;
  };

  if ($opts->{-install}) {
    Mojo::Util::monkey_patch $caller[0], $opts->{-name} => $wrapped;
    return;
  }

  Sub::Util::set_subname $subname => $wrapped;
  return $wrapped;
}

# this prototype prevents the perl tokenizer from seeing await as an
# indirect method

sub await (*) {
  {
    # check that our caller is actually an async function
    no warnings 'uninitialized';
    my $level = 1;
    my ($caller, $hints) = (caller($level))[3, 10];

    # being inside of an eval is ok too
    ($caller, $hints) = (caller(++$level))[3, 10] while $caller eq '(eval)';

    Carp::croak 'await may only be called from in async function'
      unless $hints->{'Mojo::AsyncAwait::Backend::Coro/async'};
  }

  my $promise = Mojo::Promise->resolve($_[0]);

  my (@retvals, $err);
  _pop {
    my $current = shift;
    $promise->then(
      sub {
        @retvals = @_;
        _push $current;
      },
      sub {
        $err = shift;
        _push $current;
      }
    );
  };

  # "_push $current" in the above callback brings us here
  Carp::croak($err) if $err;
  return wantarray ? @retvals : $retvals[0];
}

sub _parse_opts {
  return {} unless @_;
  return {
    -name    => shift,
    -install => 1,
  } if @_ == 1;

  my %opts = @_;
  Carp::croak 'Cannot install a sub without a name'



( run in 1.233 second using v1.01-cache-2.11-cpan-df04353d9ac )