Added ajax long polling.

This commit is contained in:
Martin Edenhofer 2012-11-27 00:22:52 +01:00
parent f496f82669
commit 5e69e1af4e
13 changed files with 314 additions and 118 deletions

View file

@ -8,8 +8,6 @@ class App.DashboardActivityStream extends App.Controller
super super
@items = [] @items = []
# refresh list ever 140 sec.
# @interval( @fetch, 1400000, 'dashboard_activity_stream' )
@fetch() @fetch()
# bind to rebuild view event # bind to rebuild view event
@ -22,17 +20,20 @@ class App.DashboardActivityStream extends App.Controller
if cache if cache
@load( cache ) @load( cache )
# # get data # init fetch via ajax, all other updates on time via websockets
# App.Com.ajax( else
# id: 'dashoard_activity_stream', App.Com.ajax(
# type: 'GET', id: 'dashoard_activity_stream'
# url: '/api/activity_stream', type: 'GET'
# data: { url: '/api/activity_stream'
# limit: @limit, data: {
# } limit: 8
# processData: true, }
# success: @load processData: true
# ) success: (data) =>
App.Store.write( 'activity_stream', data )
@load(data)
)
load: (data) => load: (data) =>
items = data.activity_stream items = data.activity_stream

View file

@ -11,19 +11,42 @@ class App.DashboardRss extends App.Controller
@fetch() @fetch()
fetch: => fetch: =>
# get data from cache
cache = App.Store.get( 'dashboard_rss' ) cache = App.Store.get( 'dashboard_rss' )
if cache if cache
@load( cache ) @render( cache )
load: (data) => # init fetch via ajax, all other updates on time via websockets
items = data.items || [] else
@head = data.head || '?' App.Com.ajax(
@render(items) id: 'dashboard_rss'
type: 'GET'
render: (items) -> url: '/api/rss_fetch'
html = App.view('dashboard/rss')( data: {
head: @head, limit: 8
items: items url: 'http://www.heise.de/newsticker/heise-atom.xml'
}
processData: true
success: (data) =>
if data.message
@render(
head: 'Heise ATOM'
message: data.message
)
else
App.Store.write( 'dashboard_rss', data )
@render(data)
error: =>
@render(
head: 'Heise ATOM'
message: 'Unable to fetch rss!'
)
)
render: (data) ->
@html App.view('dashboard/rss')(
head: data.head,
items: data.item || []
message: data.message
) )
html = $(html)
@html html

View file

@ -25,25 +25,37 @@ class App.DashboardTicket extends App.Controller
# use cache of first page # use cache of first page
cache = App.Store.get( @key ) cache = App.Store.get( @key )
if cache if cache
# @render( cache )
@load( cache ) @load( cache )
# get data # init fetch via ajax, all other updates on time via websockets
# App.Com.ajax( else
# id: 'dashboard_ticket_' + @key, App.Com.ajax(
# type: 'GET', id: 'dashboard_ticket_' + @key,
# url: '/api/ticket_overviews', type: 'GET',
# data: { url: '/api/ticket_overviews',
# view: @view, data: {
# view_mode: 'd', view: @view,
# start_page: @start_page, view_mode: 'd',
# } start_page: @start_page,
# processData: true, }
# success: @load processData: true,
# ) success: (data) =>
data.ajax = true
@load(data)
)
load: (data) => load: (data) =>
if data.ajax
data.ajax = false
App.Store.write( @key, data )
# load user collection
App.Collection.load( type: 'User', data: data.collections.users )
# load ticket collection
App.Collection.load( type: 'Ticket', data: data.collections.tickets )
# get meta data # get meta data
App.Overview.refresh( data.overview, options: { clear: true } ) App.Overview.refresh( data.overview, options: { clear: true } )
@ -57,11 +69,12 @@ class App.DashboardTicket extends App.Controller
@log 'refetch...', record @log 'refetch...', record
@fetch() @fetch()
# App.Store.write( @key, data )
@render( data ) @render( data )
render: (data) -> render: (data) ->
return if !data
return if !data.ticket_list
return if !data.overview
@overview = data.overview @overview = data.overview
@tickets_count = data.tickets_count @tickets_count = data.tickets_count

View file

@ -37,12 +37,42 @@ class Index extends App.Controller
# use cache of first page # use cache of first page
cache = App.Store.get( @key ) cache = App.Store.get( @key )
if cache if cache
@overview = cache.overview
@tickets_count = cache.tickets_count
@ticket_list = cache.ticket_list
@load(cache) @load(cache)
# init fetch via ajax, all other updates on time via websockets
else
App.Com.ajax(
id: 'ticket_overview_' + @key,
type: 'GET',
url: '/api/ticket_overviews',
data: {
view: @view,
view_mode: @view_mode,
}
processData: true,
success: (data) =>
data.ajax = true
@load(data)
)
load: (data) => load: (data) =>
return if !data
return if !data.ticket_list
return if !data.overview
@overview = data.overview
@tickets_count = data.tickets_count
@ticket_list = data.ticket_list
if data.ajax
data.ajax = false
App.Store.write( @key, data )
# load user collection
App.Collection.load( type: 'User', data: data.collections.users )
# load ticket collection
App.Collection.load( type: 'Ticket', data: data.collections.tickets )
# get meta data # get meta data
@overview = data.overview @overview = data.overview

View file

@ -5,9 +5,7 @@ class App.Com
@ajax: (args) -> # Must be a static method @ajax: (args) -> # Must be a static method
if _instance == undefined if _instance == undefined
_instance ?= new _Singleton _instance ?= new _Singleton
_instance.ajax(args) _instance.ajax(args)
_instance
# The actual Singleton class # The actual Singleton class
class _Singleton class _Singleton

View file

@ -9,7 +9,7 @@ class App.WebSocket
@close: (args) -> # Must be a static method @close: (args) -> # Must be a static method
if _instance isnt undefined if _instance isnt undefined
_instance.close() _instance.close(args)
@send: (args) -> # Must be a static method @send: (args) -> # Must be a static method
@connect() @connect()
@ -20,6 +20,7 @@ class App.WebSocket
_instance.auth(args) _instance.auth(args)
@_spool: -> @_spool: ->
@connect()
_instance.spool() _instance.spool()
# The actual Singleton class # The actual Singleton class
@ -30,6 +31,9 @@ class _Singleton extends App.Controller
supported: true supported: true
lastSpoolMessage: undefined lastSpoolMessage: undefined
connectionEstablished: false connectionEstablished: false
connectionWasEstablished: false
backend: 'websocket'
client_id: undefined
constructor: (@args) -> constructor: (@args) ->
@ -46,7 +50,10 @@ class _Singleton extends App.Controller
@connect() @connect()
send: (data) => send: (data) =>
return if !@supported if @backend is 'ajax'
@_ajaxSend(data)
else
# console.log 'ws:send trying', data, @ws, @ws.readyState # console.log 'ws:send trying', data, @ws, @ws.readyState
# A value of 0 indicates that the connection has not yet been established. # A value of 0 indicates that the connection has not yet been established.
@ -77,7 +84,7 @@ class _Singleton extends App.Controller
if @lastSpoolMessage if @lastSpoolMessage
data['timestamp'] = @lastSpoolMessage data['timestamp'] = @lastSpoolMessage
@log 'Event', 'debug', 'spool', data @log 'Websocket', 'debug', 'spool', data
# ask for spool messages # ask for spool messages
App.Event.trigger( App.Event.trigger(
@ -89,37 +96,38 @@ class _Singleton extends App.Controller
@lastSpoolMessage = Math.round( +new Date()/1000 ) @lastSpoolMessage = Math.round( +new Date()/1000 )
close: => close: =>
return if !@supported return if @backend is 'ajax'
@ws.close() @ws.close()
ping: => ping: =>
return if !@supported return if @backend is 'ajax'
@log 'Event', 'debug', 'send websockend ping' @log 'Websocket', 'debug', 'send websockend ping'
@send( { action: 'ping' } ) @send( { action: 'ping' } )
# check if ping is back within 2 min # check if ping is back within 2 min
@clearDelay('websocket-ping-check') @clearDelay('websocket-ping-check')
check = => check = =>
@log 'Event', 'notice', 'no websockend ping response, reconnect...' @log 'Websocket', 'notice', 'no websockend ping response, reconnect...'
@close() @close()
@delay check, 120000, 'websocket-ping-check' @delay check, 120000, 'websocket-ping-check'
pong: -> pong: ->
return if !@supported return if @backend is 'ajax'
@log 'Event', 'debug', 'received websockend ping'
@log 'Websocket', 'debug', 'received websockend ping'
# test again after 1 min # test again after 1 min
@delay @ping, 60000 @delay @ping, 60000
connect: => connect: =>
return if @backend is 'ajax'
if !window.WebSocket if !window.WebSocket
@error = new App.ErrorModal( @backend = 'ajax'
message: 'Sorry, no websocket support!' @log 'WebSocket', 'notice', 'no support of websocket, use ajax long polling'
) @_ajaxInit()
@supported = false
return return
protocol = 'ws://' protocol = 'ws://'
@ -130,9 +138,10 @@ class _Singleton extends App.Controller
# Set event handlers. # Set event handlers.
@ws.onopen = => @ws.onopen = =>
@log 'Event', 'notice', 'new websocked connection open' @log 'Websocket', 'notice', 'new websocket connection open'
@connectionEstablished = true @connectionEstablished = true
@connectionWasEstablished = true
# close error message show up (because try so connect again) if exists # close error message show up (because try so connect again) if exists
@clearDelay('websocket-no-connection-try-reconnect') @clearDelay('websocket-no-connection-try-reconnect')
@ -144,7 +153,7 @@ class _Singleton extends App.Controller
# empty queue # empty queue
for item in @queue for item in @queue
@log 'Event', 'debug', 'empty ws queue', item @log 'Websocket', 'debug', 'empty ws queue', item
@send(item) @send(item)
@queue = [] @queue = []
@ -153,41 +162,31 @@ class _Singleton extends App.Controller
@ws.onmessage = (e) => @ws.onmessage = (e) =>
pipe = JSON.parse( e.data ) pipe = JSON.parse( e.data )
@log 'Event', 'debug', 'ws:onmessage', pipe @log 'Websocket', 'debug', 'ws:onmessage', pipe
@_receiveMessage(pipe)
# go through all blocks
for item in pipe
# reset reconnect loop
if item['action'] is 'pong'
@pong()
# fill collection
if item['collection']
@log 'Event', 'debug', "ws:onmessage collection:" + item['collection']
App.Store.write( item['collection'], item['data'] )
# fire event
if item['event']
if typeof item['event'] is 'object'
for event in item['event']
@log 'Event', 'debug', "ws:onmessage event:" + event
App.Event.trigger( event, item['data'] )
else
@log 'Event', 'debug', "ws:onmessage event:" + item['event']
App.Event.trigger( item['event'], item['data'] )
@ws.onclose = (e) => @ws.onclose = (e) =>
@log 'Event', 'debug', "ws:onclose", e @log 'Websocket', 'debug', "ws:onclose", e
# set timestamp to get spool messages later # set timestamp to get spool messages later
if @connectionEstablished if @connectionEstablished
@lastSpoolMessage = Math.round( +new Date()/1000 ) @lastSpoolMessage = Math.round( +new Date()/1000 )
@connectionEstablished = false @connectionEstablished = false
return if @backend is 'ajax'
# show error message, first try to reconnect # show error message, first try to reconnect
if !@error if !@error
message = => message = =>
# use fallback if no connection was possible
if !@connectionWasEstablished
@backend = 'ajax'
@log 'WebSocket', 'notice', 'No connection to websocket, use use ajax long polling as fallback'
@_ajaxInit()
return
# show reconnect message
@error = new App.ErrorModal( @error = new App.ErrorModal(
message: 'No connection to websocket, trying to reconnect...' message: 'No connection to websocket, trying to reconnect...'
) )
@ -197,5 +196,110 @@ class _Singleton extends App.Controller
@delay @connect, 4500 @delay @connect, 4500
@ws.onerror = (e) => @ws.onerror = (e) =>
@log 'Event', 'debug', "ws:onerror", e @log 'Websocket', 'debug', "ws:onerror", e
_receiveMessage: (data = []) =>
# go through all blocks
for item in data
# reset reconnect loop
if item['action'] is 'pong'
@pong()
# fill collection
if item['collection']
@log 'Websocket', 'debug', "onmessage collection:" + item['collection']
App.Store.write( item['collection'], item['data'] )
# fire event
if item['event']
if typeof item['event'] is 'object'
for event in item['event']
@log 'Websocket', 'debug', "onmessage event:" + event
App.Event.trigger( event, item['data'] )
else
@log 'Websocket', 'debug', "onmessage event:" + item['event']
App.Event.trigger( item['event'], item['data'] )
_ajaxInit: (data = {}) =>
# return if init is already done and not forced
return if @_ajaxInitDone && !data.force
# stop init request if new one is started
if @_ajaxInitWorking
console.log '@_ajaxInitWorking', @_ajaxInitWorking
@_ajaxInitWorking.abort()
# call init request
@_ajaxInitWorking = App.Com.ajax(
type: 'POST'
url: '/api/message_send'
data: JSON.stringify({ data: { action: 'login' } })
processData: false
queue: false
success: (data) =>
if data.client_id
@log 'Websocket', 'notice', 'ajax:new client_id', data.client_id
@client_id = data.client_id
@_ajaxReceive()
@_ajaxSendQueue()
@_ajaxInitDone = true
@_ajaxInitWorking = false
error: =>
@_ajaxInitDone = true
@_ajaxInitWorking = false
)
_ajaxSend: (data) =>
@log 'Websocket', 'debug', 'ajax:sendmessage', data
if !@client_id || @client_id is undefined || !@_ajaxInitDone
@_ajaxInit()
@queue.push data
else
@queue.push data
@_ajaxSendQueue()
_ajaxSendQueue: =>
while !_.isEmpty(@queue)
data = @queue.shift()
App.Com.ajax(
type: 'POST'
url: '/api/message_send'
data: JSON.stringify({ client_id: @client_id, data: data })
processData: false
queue: true
success: (data) =>
if data && data.error
@client_id = undefined
@_ajaxInit( force: true )
error: =>
@client_id = undefined
@_ajaxInit( force: true )
)
_ajaxReceive: =>
return if !@client_id
return if @_ajaxReceiveWorking is true
@_ajaxReceiveWorking = true
App.Com.ajax(
id: 'message_receive',
type: 'POST'
url: '/api/message_receive'
data: JSON.stringify({ client_id: @client_id })
processData: false
success: (data) =>
@log 'Websocket', 'notice', 'ajax:onmessage', data
@_receiveMessage(data)
if data && data.error
@client_id = undefined
@_ajaxInit( force: true )
@_ajaxReceiveWorking = false
@_ajaxReceive()
error: (data) =>
@client_id = undefined
@_ajaxInit( force: true )
@_ajaxReceiveWorking = false
@delay @_ajaxReceive, 5000
)

View file

@ -1,6 +1,9 @@
<div class="row"> <div class="row">
<div class="span3 can-move"> <div class="span3 can-move">
<h2><%- @T( @head ) %></h2> <h2><%- @T( @head ) %></h2>
<% if @message: %>
<%- @T(@message) %>
<% end %>
<ul> <ul>
<% for item in @items: %> <% for item in @items: %>
<li><a href="<%= item.link %>" title="<%= item.summary %>" target="_blank"><%= item.title %>"</a></li> <li><a href="<%= item.link %>" title="<%= item.summary %>" target="_blank"><%= item.title %>"</a></li>

View file

@ -3,7 +3,7 @@ class ActivityController < ApplicationController
# GET /api/activity_stream # GET /api/activity_stream
def activity_stream def activity_stream
activity_stream = History.activity_stream_fulldata(current_user, params[:limit]) activity_stream = History.activity_stream_fulldata( current_user, params[:limit] )
# return result # return result
render :json => activity_stream render :json => activity_stream

View file

@ -20,6 +20,7 @@ curl http://localhost/api/rss_fetch.json -v -u #{login}:#{password} -H "Content-
items = RSS.fetch(params[:url], params[:limit]) items = RSS.fetch(params[:url], params[:limit])
if items == nil if items == nil
render :json => { :message => "failed to fetch #{ params[:url] }", :status => :unprocessable_entity } render :json => { :message => "failed to fetch #{ params[:url] }", :status => :unprocessable_entity }
return
end end
render :json => { :items => items } render :json => { :items => items }
end end

View file

@ -22,8 +22,8 @@ class TicketOverviewsController < ApplicationController
:array => true, :array => true,
) )
tickets = [] tickets = []
overview[:tickets].each {|ticket| overview[:tickets].each {|ticket_id|
data = { :id => ticket.id } data = { :id => ticket_id }
tickets.push data tickets.push data
} }
@ -37,17 +37,20 @@ class TicketOverviewsController < ApplicationController
end end
overview = Ticket.overview( overview = Ticket.overview(
:view => params[:view], :view => params[:view],
:view_mode => params[:view_mode], # :view_mode => params[:view_mode],
:current_user_id => current_user.id, :current_user => User.find( current_user.id ),
:start_page => params[:start_page],
:array => true, :array => true,
) )
if !overview
render :json => { :error => "No such view #{ params[:view] }!" }, :status => :unprocessable_entity
return
end
# get related users # get related users
users = {} users = {}
tickets = [] tickets = []
overview[:tickets].each {|ticket| overview[:ticket_list].each {|ticket_id|
data = Ticket.full_data(ticket.id) data = Ticket.full_data(ticket_id)
tickets.push data tickets.push data
if !users[ data['owner_id'] ] if !users[ data['owner_id'] ]
users[ data['owner_id'] ] = User.user_data_full( data['owner_id'] ) users[ data['owner_id'] ] = User.user_data_full( data['owner_id'] )
@ -84,12 +87,15 @@ class TicketOverviewsController < ApplicationController
# return result # return result
render :json => { render :json => {
:overview => overview[:overview], :overview => overview[:overview],
:tickets => tickets, :ticket_list => overview[:ticket_list],
:tickets_count => overview[:tickets_count], :tickets_count => overview[:tickets_count],
:users => users,
:bulk => { :bulk => {
:group_id__owner_id => groups_users, :group_id__owner_id => groups_users,
}, },
:collections => {
:users => users,
:tickets => tickets,
},
} }
end end

View file

@ -217,6 +217,10 @@ class Ticket < ApplicationModel
} }
} }
if data[:view] && !overview_selected
return
end
# sortby # sortby
# prio # prio
# state # state
@ -290,7 +294,7 @@ class Ticket < ApplicationModel
count() count()
return { return {
:tickets => ticket_ids, :ticket_list => ticket_ids,
:tickets_count => tickets_count, :tickets_count => tickets_count,
:overview => overview_selected_raw, :overview => overview_selected_raw,
} }

View file

@ -29,6 +29,7 @@ module RSS
rescue Exception => e rescue Exception => e
puts "can't fetch #{url}" puts "can't fetch #{url}"
puts e.inspect puts e.inspect
return
end end
return items return items

View file

@ -40,6 +40,16 @@ module Session
return session_list return session_list
end end
def self.touch( client_id )
data = self.get(client_id)
path = @path + '/' + client_id.to_s
data[:meta][:last_ping] = Time.new.to_i.to_s
File.open( path + '/session', 'w' ) { |file|
file.puts Marshal.dump(data)
}
return true
end
def self.get( client_id ) def self.get( client_id )
session_file = @path + '/' + client_id.to_s + '/session' session_file = @path + '/' + client_id.to_s + '/session'
data = nil data = nil
@ -105,6 +115,7 @@ module Session
@@user_threads[user.id] = Thread.new { @@user_threads[user.id] = Thread.new {
UserState.new(user.id) UserState.new(user.id)
@@user_threads[user.id] = nil @@user_threads[user.id] = nil
puts "close user(#{user.id}) thread"
# raise "Exception from thread" # raise "Exception from thread"
} }
end end
@ -119,13 +130,14 @@ module Session
@@client_threads[client_id] = Thread.new { @@client_threads[client_id] = Thread.new {
ClientState.new(client_id) ClientState.new(client_id)
@@client_threads[client_id] = nil @@client_threads[client_id] = nil
puts "close client(#{client_id}) thread"
# raise "Exception from thread" # raise "Exception from thread"
} }
end end
} }
# system settings # system settings
sleep 0.4 sleep 0.5
end end
end end
@ -464,7 +476,7 @@ class ClientState
self.log 'notify', "push overview_data #{overview.meta[:url]} for user #{user.id}" self.log 'notify', "push overview_data #{overview.meta[:url]} for user #{user.id}"
users = {} users = {}
tickets = [] tickets = []
overview_data[:tickets].each {|ticket_id| overview_data[:ticket_list].each {|ticket_id|
self.ticket( ticket_id, tickets, users ) self.ticket( ticket_id, tickets, users )
} }
@ -494,7 +506,7 @@ class ClientState
self.send({ self.send({
:data => { :data => {
:overview => overview_data[:overview], :overview => overview_data[:overview],
:ticket_list => overview_data[:tickets], :ticket_list => overview_data[:ticket_list],
:tickets_count => overview_data[:tickets_count], :tickets_count => overview_data[:tickets_count],
:collections => { :collections => {
:User => users, :User => users,