use strict; use warnings; package Jifty::Plugin::PubSub::Connection; =head1 NAME Jifty::Plugin::PubSub::Connection - Connection to browser =head1 DESCRIPTION This class represents a bidirectional channel between the server and the web browser. You may wish to subclass this class as C to override the L, L, or L methods. =head1 METHODS =cut # This is _new rather than new because it should never be called by # external code sub _new { my $class = shift; my $env = shift; my $self = bless {}, $class; $self->{web} = Jifty::Web->new; $self->{web}->request( Jifty::Request->promote( Plack::Request->new( $env ) ), ); $self->{web}->response( Jifty::Response->new ); { local $Jifty::WEB = $self->{web}; Jifty->web->setup_session; } $self->{api} = Jifty::API->new; $self->{listener} = $env->{'hippie.listener'}; $self->{client_id} = $env->{'hippie.client_id'}; $self->{region_subs} = []; for my $sub ( @{ Jifty->subs->retrieve($self->client_id) }) { if ( join(" ", keys %{$sub}) eq "topic" ) { $self->subscribe( $sub->{topic} ); } else { push @{ $self->{region_subs} }, $sub; } } if ( @{ $self->{region_subs} } ) { my @subs = map {$_->{topic}} @{ $self->{region_subs} }; $self->{region_bus} = Jifty->bus->new_listener; $self->{region_bus}->subscribe( $_ ) for map {Jifty->bus->topic($_)} @subs; $self->{region_bus}->poll( sub { $self->region_event( @_ ) } ); } $self->subscribe( "client." . $self->client_id ); return $self; } =head2 connect Called when a connection is established from the browser. By default, does nothing. =cut sub connect {} =head2 subscribe I [, I, ...] Subscribes the browser to receive messages on the given topics. =cut sub subscribe { my $self = shift; $self->{listener}->subscribe( $_ ) for map { Jifty->bus->topic( $_) } @_; } =head2 send I I Sends an arbitrary message to the browser. It is not published to the rest of the message bus. =cut sub send { my $self = shift; my ($type, $data) = @_; $data->{type} = $type; Jifty->bus->topic("client." . $self->client_id ) ->publish( $data ); } =head2 receive I Called when a message is received from the web browser; returns true if the message was processed, false otherwise. If you override this method, be sure you respect this class' return value: sub receive { my $self = shift; my $msg = shift; return 1 if $self->SUPER::receive( $msg ); # ... } =cut sub receive { my $self = shift; my $msg = shift; return 1 if $self->action_message($msg); return; } =head2 disconnect Called when the connection to the browser is lost when the browser switches to a new page. This is not immediate, but occurs after a 15-second timeout. =cut sub disconnect { my $self = shift; if ($self->{region_bus}) { $self->{region_bus}->timeout(0); $self->{region_bus}->unpoll; undef $self->{region_bus}; }; } =head2 web Returns the constructed L object which is seen as Cweb> whenever in the context of this connection's L, L, L, or when page regions are rendered to be sent over this channel. This ensures that Cweb->current_user> is set whenever it is relevant. =head2 api A new L object is instantiated for each L object. You may wish to limit it to limit which actions can be performed by the web browser. =head2 listener The L object which listens to events for the client. =head2 client_id Returns a unique identifier associated with this connection. =cut sub web { shift->{web} } sub api { shift->{api} } sub listener { shift->{listener} } sub client_id { shift->{client_id} } =head2 action_message I Creates, validates, and runs an action if it was received by the client; called by L. =cut sub action_message { my $self = shift; my $msg = shift; return unless ($msg->{type} || '') eq "jifty.action" and defined $msg->{class}; my $class = Jifty->api->qualify($msg->{class}); unless (Jifty->api->is_allowed($class)) { warn "Attempt to call denied action $class: ".Jifty->api->explain($class); return 1; } my $action = Jifty->web->new_action( class => $class, arguments => $msg->{arguments} || {}, ); $action->validate; $action->run if $action->result->success; my $result = $action->result->as_hash; $self->send( "jifty.result" => $result); return 1; } =head2 region_event I Called when one or more regions on the page needs to be rendered and pushed to the client, as triggered by an event. The rendered regions will be passed I as an C variable. Currently, rendered regions cannot alter the client's subscription set. =cut sub region_event { my $self = shift; my $event = shift; my $type = $event->{type} or return; local $Jifty::WEB = $self->web; local $Jifty::API = $self->api; Jifty::Record->flush_cache if Jifty::Record->can('flush_cache'); for my $sub ( @{$self->{region_subs}} ) { next unless $sub->{topic} eq $type; my $content; my $region_name; eval { # So we don't warn about "duplicate region"s local Jifty->web->{'regions'} = {}; local Jifty->web->{'region_stack'} = []; # XXX TODO: the "first page render" method of storing subs # in Jifty->subs doesn't work when we're in the connection # context; no-op all such attempts. We _can_ nominally # alter them immediately using $self->subscribe, however; # shim in a Jifty->subs which wraps ->add and turns it into # what we do in ->_new local Jifty->subs->{store} = {}; my $region = Jifty::Web::PageRegion->new( name => $sub->{region}, path => $sub->{path}, arguments => $sub->{arguments}, ); $region_name = $region->qualified_name; $region->enter; Jifty->handler->buffer->push( private => 1 ); $region->render_as_subrequest( { %{$region->arguments}, event => $event, } ); $content = Jifty->handler->buffer->pop; $region->exit; 1; } or warn "$@"; $self->send( "jifty.fragment" => { region => $region_name, path => $sub->{path}, args => $sub->{arguments}, content => $content, mode => $sub->{mode}, element => $sub->{element}, %{ $sub->{attrs} || {} }, } ); } # For some reason, AnyMQ makes a queue sub'd to the topic become # undef after the poll? Re-subscribing resolves the issue. my @subs = map {$_->{topic}} @{ $self->{region_subs} }; $self->{region_bus}->subscribe( $_ ) for map {Jifty->bus->topic($_)} @subs; } 1;