## RSence
# Copyright 2008 Riassence Inc.
# http://riassence.com/
#
# You should have received a copy of the GNU General Public License along
# with this software package. If not, contact licensing@riassence.com
##
# The MainPlugin is accessible as +@plugins.main+ and just +main+ from other plugins.
#
# = MainPlugin provides mainly client setup and the following services:
# * The root html page, which includes the scripts and sets up client startup variables.
# * The url of the client as a HValue, including the anchor.
# * Accessible via +msg.session[:main][:location_href]+
# * The local time of the client's web browser as a HValue, as seconds since epoch.
# * Accessible via +msg.session[:main][:client_time]+
# * Sequential loading. See {#delayed_call #delayed_call}
# * Provides the +#init_ui+ event for plugins that respond to it.
class MainPlugin < Plugin
# # Session-specific index page renderer (unused)
# def session_index_html( request, response )
#
# ses_key = @randgen.gen
# sha_key = ''
#
# buffer = [
# "var qP=function(cmd){COMM.Queue.push(cmd);};"
# ]
#
# req_num = 0
#
# 3.times do |req_num|
# sha_key = Digest::SHA1.hexdigest( ses_key + sha_key )
# msg = @plugins.transporter.xhr(
# request, response, {
# :servlet => true,
# :cookie => (req_num==0),
# :query => {
# 'ses_key' => "#{req_num}:1:#{sha_key}"
# }
# }
# )
# buffer += msg.value_buffer
# msg.buffer.each do |buffer_item|
# buffer.push( "qP(function(){#{buffer_item};});")
# end
# ses_key = msg.ses_key
# end
#
# buffer.unshift( "COMM.Session.newKey(#{ses_key.to_json});" )
# buffer.unshift( "COMM.Session.sha_key=#{sha_key.to_json};" )
# buffer.unshift( "COMM.Session.req_num=#{req_num};" )
#
# index_html = render_index_html
#
# return index_html.gsub('__STARTUP_SEQUENCE__', buffer.join("\n") )
# end
# Index page renderer
def render_index_html
index_html = @index_html_src.clone
client_rev = client_pkg.client_cache.client_rev
deps_src = ''
@conf[:deps].each do |dep|
deps_src += %{}
end
deps_src += %{}
@conf[:default_libs].each do |dep|
deps_src += %{}
end
client_base = File.join(@bconf[:h],client_rev)
index_html.gsub!( '__SCRIPT_DEPS__', deps_src )
index_html.gsub!( '__CLIENT_BASE__', client_base )
index_html.gsub!( '__DEFAULT_TITLE__', @conf[:title] )
index_html.gsub!( '__CLIENT_REV__', client_rev )
index_html.gsub!( '__CLIENT_HELLO__', @bconf[:hello] )
index_html.gsub!( '__NOSCRIPT__', @conf[:noscript] )
return index_html
end
### Top-level plugin events:
# Binds configuration data as instance variables
def init
super
@plugins.register_alias( :main, :index_html )
@randgen = RandGen.new( 40 )
::RSence.config[:index_html][:instance] = self
@conf = ::RSence.config[:index_html]
@bconf = ::RSence.config[:broker_urls]
@goodbye_uri = File.join(@bconf[:hello],'goodbye')
end
# Opens and renders the index page template
def open
super
@index_html_src = file_read( ::RSence.config[:index_html][:index_tmpl] )
render_index_html
end
# Frees the ticket resource id of the "loading" gif image.
def close
super
@plugins[:ticket].del_rsrc( @loading_gif_id )
end
### Servlet features; responds to GET / as well as POST /hello/goodbye
# @private Internal structures, matches the "hello/goodbye" session termination POST request and the "/" index html page GET request
def match( uri, method )
if uri == ::RSence.config[:index_html][:respond_address] and ( method == :get or method == :head )
return true
elsif method == :post and uri == @goodbye_uri
return true
else
return false
end
end
# @private Internal structures, score for the "hello/goodbye" session termination request and the default index html page
def score
return 1000 # allows overriding with anything with a score below 1000
end
# Inspects the http request header to decide if the browser supports gzip compressed responses.
def support_gzip( header )
return false if not ::RSence.config[:no_gzip]
return false if not header.has_key?('accept-encoding')
return header['accept-encoding'].include?('gzip')
end
# Outputs the startup web page.
def get( req, response, ses )
index_html = render_index_html
response.status = 200
response['Content-Type'] = 'text/html; charset=UTF-8'
response['Date'] = httime( Time.now )
response['Server'] = 'RSence'
response['Cache-Control'] = 'no-cache'
if support_gzip( req.header )
index_gzip = GZString.new('')
gzwriter = Zlib::GzipWriter.new( index_gzip, 9 )
gzwriter.write( index_html )
gzwriter.close
response['Content-Length'] = index_gzip.bytesize.to_s
response['Content-Encoding'] = 'gzip'
response.body = index_gzip
else
response['Content-Length'] = index_html.bytesize.to_s
response.body = index_html
end
end
# Returns the "hello/goodbye" session termination request
def post( req, res, ses )
@plugins.sessions.expire_ses_by_req( req, res )
end
### Features accessible from other plugins:
# The +#url_responder+ gets called whenever the anchor (pound) of location.href changes.
# It enables virtual url events for back/forward buttons and bookmarking in browsers whenever utilized.
#
# Client-side support is included in js/url_responder.js
#
# Also allows virtual-host -like behavior if utilized.
def url_responder(msg,location_href)
ses = get_ses( msg )
# Virtual locations:
if location_href.data.include?('#')
# split 'http://localhost:8001/#/some_uri'
# -> ['http://localhost:8001/','/some_uri']
ses[:url] = location_href.data.split('#')
virtual_uri = ses[:url][1]
# built-in support for signing out, deletes the
# server-side session and reloads the page
if virtual_uri == '/sign_out'
resp_addr = @conf[:respond_address]
msg.expire_session()
msg.reply( [
'COMM.Transporter.stop=true;',
"location.href=#{resp_addr.to_json};"
].join('') )
end
else
ses[:url] = [location_href.data,nil]
end
# url_responder always accepts locations
return true
end
# Returns base url of browser (before the '#' sign)
def url( msg )
get_ses( msg )[:url][0]
end
# Returns pound url of browser (after the '#' sign)
def pound( msg )
get_ses( msg )[:url][1]
end
### Session events:
# New session initialization, called just once per session.
def init_ses( msg )
super
restore_ses( msg )
end
def index_deps_setup( msg )
ses = msg.session
if not ses.has_key?( :deps )
# make an array of dependencies for this session, if not already done
ses[:deps] = []
end
compound_pkgs = RSence.config[:client_pkg][:compound_packages]
boot_dep = @conf[:boot_lib]
unless ses[:deps].include?( boot_dep )
if compound_pkgs.include?( boot_dep )
compound_pkgs[ boot_dep ].each do |pkg_name|
ses[:deps].push( pkg_name )
msg.reply(%{jsLoader.loaded("#{pkg_name}");})
end
end
ses[:deps].push( boot_dep )
begin
msg.reply(%{jsLoader.loaded("#{boot_dep}");})
rescue => e
warn %{ses_id: #{msg.ses_id} failed to load boot_dep: "#{boot_dep}", because: #{e.inspect}}
end
if boot_dep == 'rsence'
ses[:deps].push( 'std_widgets' )
msg.reply(%{jsLoader.loaded("std_widgets");})
end
end
@conf[:default_libs].each do |dep_lib|
unless ses[:deps].include?( dep_lib )
if compound_pkgs.include?( dep_lib )
compound_pkgs[ dep_lib ].each do |pkg_name|
ses[:deps].push( pkg_name )
msg.reply(%{jsLoader.loaded("#{pkg_name}");})
end
end
ses[:deps].push( dep_lib )
msg.reply(%{jsLoader.loaded("#{dep_lib}");})
end
end
end
# Called once when a session is restored or cloned using the cookie's ses_key
def restore_ses( msg )
super
index_deps_setup( msg )
## Resets session data to defaults
ses = get_ses( msg )
ses[:boot] = 0
ses[:url] = [nil,nil]
ses[:delayed_calls] = []
ses[:poll_mode] = true
end
# Interface for adding delayed calls
#
# When adding a delayed call, use an Array to define a plugin/method with optional arguments that will be called on the next request. The client will call back immediately when a delayed call is pending. The first param of the method is a +msg+. Don't include the +msg+ of the current request in params, it will be inserted automatically for the delayed call.
#
# It can also be used for loading sequences to the client, when using a String as the +params+.
#
# == Format of +params+ for plugin callback:
# Array:: [ plugin_name, method_name, *args ]
#
# == Format of +params+ for javascript sequences:
# String:: Javascript to send
#
# Calls will be flushed per request with the following conditions:
# * At most four (4) delayed calls will be processed at a time
# * If the calls use more than 200ms combined, even less will be processed at a time
#
# @param [Message] msg The message instance.
# @param [Array, String] params The params of the delayed call.
#
# @return [nil]
def delayed_call( msg, params )
get_ses( msg )[:delayed_calls].push( params )
end
# @private Initializes the client-side COMM.urlResponder and sesWatcher
def boot0( msg, ses )
msg.reply read_js( 'main' )
msg.reply("ELEM.setStyle(0,'background-color','#{::RSence.config[:main_plugin][:bg_color]}');")
## url_responder is bound in the client-space
## to tell the server its status by updating its value
location_href_id = ses[:location_href].val_id.to_json
msg.reply "try{COMM.Values.values[#{location_href_id}].bind(COMM.urlResponder);}catch(e){console.log('urlResponder failed, reason:',e);}"
## This enables SesWatcher that changes :client_time every n seconds, which depends on the server_poll_interval configuration setting.
## It makes the client to poll the server on regular intervals, when polling mode is disabled.
# 5000ms = 5secs
client_time_id = ses[:client_time].val_id.to_json
poll_interval = ::RSence.config[:main_plugin][:server_poll_interval]
msg.reply "try{window.sesWatcher=COMM.SessionWatcher.nu(#{poll_interval},#{client_time_id});}catch(e){console.log('sesWatcher failed, reason:',e);}"
end
# @private Calls the init_ui method of each loaded plugin and removes the loading -message
def boot1( msg, ses )
# Delegates the init_ui method to each plugin to signal bootstrap completion.
msg.plugins.delegate( 'init_ui', msg ) unless ses[:dont_init_ui]
end
# Disables the init_ui event.
def dont_init_ui( msg )
get_ses( msg )[:dont_init_ui] = true
end
# Enables the init_ui event.
def do_init_ui( msg )
get_ses( msg )[:dont_init_ui] = false
end
# Flushes commands in the :delayed_calls array
def flush_delayed( msg, ses )
## Limits the amount of delayed calls to process to 4.
## Prevents the client from choking even when the server
## load is light.
if ses[:delayed_calls].size < 4
call_count = ses[:delayed_calls].size
else
call_count = 4
end
time_start = Time.now.to_f
time_taken = 0.0
## process delayed calls, until:
## - over 200ms of cpu time has been spent
## - the :delayed_calls -array is empty
## - call_count limit is reached
until time_taken > 0.2 or ses[:delayed_calls].size == 0 or call_count == 0
# gets the next call
delayed_call = ses[:delayed_calls].shift
if RSence.args[:debug]
puts "delayed_call: #{delayed_call.inspect}"
end
# strings are always javascript, used for segmenting client load
if delayed_call.class == String
msg.reply delayed_call
# arrays are plugin calls
elsif delayed_call.class == Array
# ['plugin_name', 'method_name'] pairs call the named plugin:method with just msg
if delayed_call.size == 2
(plugin_name,method_name) = delayed_call
msg.run(plugin_name,method_name,msg)
# if the array contains more items, they are used as additional method params
else
(plugin_name,method_name) = delayed_call[0..1]
method_params = delayed_call[2..-1]
msg.run(plugin_name,method_name,msg,*method_params)
end
end
## calculates time taken
time_taken = Time.now.to_f - time_start
call_count -= 1
end
## Sets the client into poll mode, unless the :delayed_calls -array is empty
if ses[:boot] > 1
if ses[:delayed_calls].empty?
end_polling( msg, ses )
else
start_polling( msg, ses )
end
end
end
# When nothing is delayed and the second poll has been made (init_ui called),
# sets the client to non-polling-mode, having only value synchronization trigger
# new requests. On the client, SesWatcher forces the change by sending the
# client time periodically.
def end_polling( msg, ses )
if ses[:poll_mode] == true
msg.reply "COMM.Transporter.poll(0);"
ses[:poll_mode] = false
end
end
# Starts polling mode.
def start_polling( msg, ses )
if ses[:poll_mode] == false
msg.reply( "COMM.Transporter.poll(#{::RSence.config[:transporter_conf][:client_poll_priority]});" )
ses[:poll_mode] = true
end
end
# Called on every request of an active, valid session
def idle(msg)
ses = get_ses( msg )
if ses[:boot] == 0
boot0( msg, ses )
elsif ses[:boot] == 1
boot1( msg, ses )
elsif not ses[:delayed_calls].empty?
flush_delayed( msg, ses )
elsif ses[:boot] > 1
end_polling( msg, ses )
end
## Increment the counter forever.
ses[:boot] += 1
end
end