The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Event::Join;
use Moose;
use List::Util qw(reduce first);

our $VERSION = '0.06';

has 'events' => (
    is         => 'ro',
    isa        => 'ArrayRef[Str]',
    required   => 1,
    auto_deref => 1,
);

has 'on_event' => (
    is       => 'ro',
    isa      => 'CodeRef',
    default  => sub { sub {} },
    required => 1,
);

has 'on_completion' => (
    is       => 'ro',
    isa      => 'CodeRef',
    required => 1,
);

has 'received_events' => (
    traits   => ['Hash'],
    is       => 'ro',
    isa      => 'HashRef',
    default  => sub { +{} },
    required => 1,
    handles  => {
        'send_event' => 'set',
        'event_sent' => 'exists',
    },
);

sub _check_event_name {
    my ($self, $event_name) = @_;
    confess "'$event_name' is an unknown event"
      unless first { $event_name eq $_ } $self->events;
}

before send_event => sub {
    my ($self, $event_name) = @_;
    confess "Already sent event '$event_name'"
      if $self->event_sent($event_name);

    $self->_check_event_name($event_name);
};

around send_event => sub {
    my ($orig, $self, $event_name, $value) = @_;
    $self->$orig($event_name, $value);
};

after send_event => sub {
    my ($self, @args) = @_;

    $self->on_event->(@args);

    my $done = reduce { $a && $b } (
        1, map { $self->event_sent($_) } $self->events,
    );

    if($done){
        $self->on_completion->( $self->received_events );
    }
};

sub event_sender_for {
    my ($self, $event) = @_;
    $self->_check_event_name($event);
    return sub {
        $self->send_event($event, @_);
    };
}

1;

__END__

=head1 NAME

Event::Join - join multiple "events" into one

=head1 SYNOPSIS

    use Event::Join;

    my $joiner = Event::Join->new(
        on_completion => sub {
            my $events = shift;
            say 'Child exited with status '. $events->{child_done};
        },
        events => [qw/stdout_closed child_done/],
    );

    watch_fh $stdout, on_eof  => sub { $joiner->send_event('stdout_closed') };
    watch_child $pid, on_exit => sub { $joiner->send_event('child_done', $_[0]) };

    start_main_loop;

=head1 DESCRIPTION

When writing event-based programs, you often want to wait for a number
of events to occur, and then do something.  This module allows you to
do that without blocking.  It simply acts as a receiver for a number
of events, and then calls a callback when all events have occurred.

Note that although I mainly use this for "real" event-based
programming, the technique is rather versatile.  A config file parser
could be implemented like this:

   my $parsed_doc;
   my $parser_state = Event::Join->new(
       events        => [qw/username password machine_name/],
       on_completion => sub { $parsed_doc = shift },
   );

   while(!$parsed_doc && (my $line = <$fh>)){
       chomp $line;
       my ($k, $v) = split /:/, $line;
       $parser_state->send_event($k, $v);
   }

   say 'Username is '. $parsed_doc->{username};

=head1 METHODS

=head2 new

Create an instance.  Needs to be passed C<events>, an arrayref of
valid event names, and C<on_completion>, a coderef to call after all
events have been received.  This coderef is passed a hashref of events
and their values, and will only ever be called once (or not at all, if
the events never arrive).

=head2 send_event( $event_name, [$event_value] )

Send an event.  C<$event_name> is required, and must be an event that
was passed to the constructor.  An exception will be thrown if the
name is not valid.

C<$event_value> is optional; is is the value that goes into the hash
to be passed to the callback.  It can be true or false -- its value
does not affect whether or not the completino callback is called.

Finally, an exception is thrown if an event is sent more than once.

=head2 event_sent( $event_name )

Returns true if the event has been sent, false otherwise.  Note that
the true value is I<not> the value that was passed to C<send_event>,
it is just an arbitrary non-false value.

=head2 event_sender_for( $event_name )

Returns a coderef that sends C<$event_name> when run.  The first
argument to the coderef will become the second argument to
C<send_event>.

=head1 PATCHES

Is the module totally broken?  Patch my repository at:

    http://github.com/jrockway/event-join

=head1 AUTHOR

Jonathan Rockway C<< <jrockway@cpan.org> >>

=head1 COPYRIGHT

Copyright (c) 2009 Jonathan Rockway.

This module is Free Software.  You may distribute it under the same
terms as Perl itself.