The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use warnings;
use strict;

package Jifty::Subs;

use base qw/Jifty::Object/;

use constant new => __PACKAGE__;

=head1 NAME

Jifty::Subs - Configure subscriptions for the current window or session

=head1 SYNOPSIS

 my $sid = Jifty->subs->add(
    class       => 'Tick',
    queries     => [{ like => '9' }],
    mode        => 'Replace',
    region      => "clock-time",
    render_with => '/fragments/time',
 );
 Jifty->subs->cancel($sid);

 my @sids = Jifty->subs->list;

=head1 DESCRIPTION



=head1 METHODS

=head2 add PARAMHASH

Add a subscription for the current window or session.

Takes the following parameters

=over

=item class

What class of object shall we subscribe to notifications on

=item queries

An array of queries to match items of class C<class> against. The implementation of C<queries> is dependent on the type of object events are being recorded against

=item mode

How should the fragment sent to the client on matching events be rendered. Valid modes are C<Replace>, C<Bottom> and C<Top>

=item region

The moniker of the region that updates to this subscription should be rendered into

=item render_with

The path of the fragment used to render items matching this subscription

=item effect

The effect to use when showing the region, if any.

=item effect_args

Arguments to the effect

=item remove_effect

The effect to use when removing the old value of the region, if any.

=item remove_effect_args

Arguments to the remove effect

=item coalesce

If multiple events would cause the update of the given region with the
same C<render_with> path, merge them and only render the region once,
with the latest update.

=back

=cut

sub add {
    my $class = shift;
    my $args = {@_};
    unless (Jifty->config->framework('PubSub')->{'Enable'}) {
        $class->log->error("PubSub disabled, but $class->add called");
        return undef;
    }

    $args->{coalesce} = 1 if not exists $args->{coalesce} and $args->{mode} eq "Replace";

    my $id          = ($args->{window_id} || Jifty->web->session->id);
    my $event_class = $args->{class};
    $event_class = Jifty->app_class("Event", $args->{class}) unless $event_class =~ /^Jifty::Event::/;

    my $queries = $args->{queries} || [];
    my $region  = $args->{region};
    my $channel = $event_class->encode_queries(@$queries);
    $args->{attrs}{$_} = delete $args->{$_}
        for grep {exists $args->{$_}} qw/effect effect_args remove_effect remove_effect_args/;

    # The ->modify here is calling into the callback sub{...} with
    # the previous value of $_, that is a hashref of channels to
    # queries associated with those channels.  The callback then
    # massages it to add a new channel/queries mapping; the value
    # of $_ at the end of the callback is then atomically updated
    # into the message bus under the same key.
    Jifty->bus->modify(
        "$event_class-subscriptions" => sub {
            $_->{$channel} = $queries;
        }
    );

    # The per-window/session ($id) rendering information ("$id-render")
    # contains a hash from subscribed channels to rendering information,
    # including the frament, region, argument and ajax updating mode.
    Jifty->bus->modify(
        "$id-render" => sub {
            $_->{$channel}{$region} = {
                map { $_ => $args->{$_} }
                    qw/render_with region arguments mode coalesce attrs/
            };
        }
    );

    # We create/update a IPC::PubSub::Subscriber object for this $id,
    # and have it subscribe to the channel that we're adding here.
    Jifty->bus->modify(
        "$id-subscriber" => sub {
            if   ($_) { $_->subscribe($channel) }
            else      { $_ = Jifty->bus->new_subscriber($channel) }
        }
    );

    return "$channel!$id!$region";
}

=head2 cancel CHANNEL_ID

Cancels session or window's subscription to CHANNEL_ID

=cut

sub cancel {
    my ($class, $channel_id) = @_;

    unless (Jifty->config->framework('PubSub')->{'Enable'}) {
        $class->log->error("PubSub disabled, but $class->cancel called");
        return undef;
    }

    my ($channel, $id, $region) = split(/!/, $channel_id, 3);
    my ($event_class)  = split(/-/, $channel);

    $id ||= Jifty->web->session->id;

    my $last;
    Jifty->bus->modify(
        "$id-render" => sub {
            delete $_->{$channel}{$region};
            $last = 1 unless %{$_->{$channel}};
        }
    );

    if ($last) {
        Jifty->bus->modify(
            "$event_class-subscriptions" => sub {
                delete $_->{$channel};
            }
        );

        Jifty->bus->modify(
            "$id-subscriber" => sub {
                if ($_) { $_->unsubscribe($channel) }
            }
        );
    }
}

=head2 list [window/sessionid]

Returns a lost of channel ids this session or window is subscribed to.

=cut


sub list {
    my $class = shift;

    unless (Jifty->config->framework('PubSub')->{'Enable'}) {
        $class->log->error("PubSub disabled, but $class->add called");
        return undef;
    }

    my $id   = (shift || Jifty->web->session->id);
    my $subscribe = Jifty->bus->modify( "$id-subscriber" ) or return ();
    return $subscribe->channels;
}

=head2 update_on PARAMHASH

Like L</add>, but provides a sane set of defaults for updating the
current region, based on inspection of the current region's
properties.  C<queries> is set to the region's arguments, and C<class>
is left unspecified.

=cut

sub update_on {
    my $class = shift;

    my $region = Jifty->web->current_region;
    unless ($region) {
        warn "Jifty->subs->update_on called when not in a region";
        return;
    }

    my %args = %{ $region->arguments };
    delete $args{region};
    delete $args{event};

    $class->add(
        queries     => [ \%args ],
        arguments   => \%args,
        mode        => 'Replace',
        region      => $region->qualified_name,
        render_with => $region->path,
        @_,
    );
}

1;