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

View file

@ -11,19 +11,42 @@ class App.DashboardRss extends App.Controller
@fetch()
fetch: =>
# get data from cache
cache = App.Store.get( 'dashboard_rss' )
if cache
@load( cache )
@render( cache )
load: (data) =>
items = data.items || []
@head = data.head || '?'
@render(items)
render: (items) ->
html = App.view('dashboard/rss')(
head: @head,
items: items
# init fetch via ajax, all other updates on time via websockets
else
App.Com.ajax(
id: 'dashboard_rss'
type: 'GET'
url: '/api/rss_fetch'
data: {
limit: 8
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
cache = App.Store.get( @key )
if cache
# @render( cache )
@load( cache )
# get data
# App.Com.ajax(
# id: 'dashboard_ticket_' + @key,
# type: 'GET',
# url: '/api/ticket_overviews',
# data: {
# view: @view,
# view_mode: 'd',
# start_page: @start_page,
# }
# processData: true,
# success: @load
# )
# init fetch via ajax, all other updates on time via websockets
else
App.Com.ajax(
id: 'dashboard_ticket_' + @key,
type: 'GET',
url: '/api/ticket_overviews',
data: {
view: @view,
view_mode: 'd',
start_page: @start_page,
}
processData: true,
success: (data) =>
data.ajax = true
@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
App.Overview.refresh( data.overview, options: { clear: true } )
@ -57,11 +69,12 @@ class App.DashboardTicket extends App.Controller
@log 'refetch...', record
@fetch()
# App.Store.write( @key, data )
@render( data )
render: (data) ->
return if !data
return if !data.ticket_list
return if !data.overview
@overview = data.overview
@tickets_count = data.tickets_count

View file

@ -37,12 +37,42 @@ class Index extends App.Controller
# use cache of first page
cache = App.Store.get( @key )
if cache
@overview = cache.overview
@tickets_count = cache.tickets_count
@ticket_list = cache.ticket_list
@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) =>
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
@overview = data.overview

View file

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

View file

@ -9,7 +9,7 @@ class App.WebSocket
@close: (args) -> # Must be a static method
if _instance isnt undefined
_instance.close()
_instance.close(args)
@send: (args) -> # Must be a static method
@connect()
@ -20,6 +20,7 @@ class App.WebSocket
_instance.auth(args)
@_spool: ->
@connect()
_instance.spool()
# The actual Singleton class
@ -30,6 +31,9 @@ class _Singleton extends App.Controller
supported: true
lastSpoolMessage: undefined
connectionEstablished: false
connectionWasEstablished: false
backend: 'websocket'
client_id: undefined
constructor: (@args) ->
@ -46,7 +50,10 @@ class _Singleton extends App.Controller
@connect()
send: (data) =>
return if !@supported
if @backend is 'ajax'
@_ajaxSend(data)
else
# console.log 'ws:send trying', data, @ws, @ws.readyState
# A value of 0 indicates that the connection has not yet been established.
@ -77,7 +84,7 @@ class _Singleton extends App.Controller
if @lastSpoolMessage
data['timestamp'] = @lastSpoolMessage
@log 'Event', 'debug', 'spool', data
@log 'Websocket', 'debug', 'spool', data
# ask for spool messages
App.Event.trigger(
@ -89,37 +96,38 @@ class _Singleton extends App.Controller
@lastSpoolMessage = Math.round( +new Date()/1000 )
close: =>
return if !@supported
return if @backend is 'ajax'
@ws.close()
ping: =>
return if !@supported
return if @backend is 'ajax'
@log 'Event', 'debug', 'send websockend ping'
@log 'Websocket', 'debug', 'send websockend ping'
@send( { action: 'ping' } )
# check if ping is back within 2 min
@clearDelay('websocket-ping-check')
check = =>
@log 'Event', 'notice', 'no websockend ping response, reconnect...'
@log 'Websocket', 'notice', 'no websockend ping response, reconnect...'
@close()
@delay check, 120000, 'websocket-ping-check'
pong: ->
return if !@supported
@log 'Event', 'debug', 'received websockend ping'
return if @backend is 'ajax'
@log 'Websocket', 'debug', 'received websockend ping'
# test again after 1 min
@delay @ping, 60000
connect: =>
return if @backend is 'ajax'
if !window.WebSocket
@error = new App.ErrorModal(
message: 'Sorry, no websocket support!'
)
@supported = false
@backend = 'ajax'
@log 'WebSocket', 'notice', 'no support of websocket, use ajax long polling'
@_ajaxInit()
return
protocol = 'ws://'
@ -130,9 +138,10 @@ class _Singleton extends App.Controller
# Set event handlers.
@ws.onopen = =>
@log 'Event', 'notice', 'new websocked connection open'
@log 'Websocket', 'notice', 'new websocket connection open'
@connectionEstablished = true
@connectionWasEstablished = true
# close error message show up (because try so connect again) if exists
@clearDelay('websocket-no-connection-try-reconnect')
@ -144,7 +153,7 @@ class _Singleton extends App.Controller
# empty queue
for item in @queue
@log 'Event', 'debug', 'empty ws queue', item
@log 'Websocket', 'debug', 'empty ws queue', item
@send(item)
@queue = []
@ -153,41 +162,31 @@ class _Singleton extends App.Controller
@ws.onmessage = (e) =>
pipe = JSON.parse( e.data )
@log 'Event', 'debug', 'ws:onmessage', 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'] )
@log 'Websocket', 'debug', 'ws:onmessage', pipe
@_receiveMessage(pipe)
@ws.onclose = (e) =>
@log 'Event', 'debug', "ws:onclose", e
@log 'Websocket', 'debug', "ws:onclose", e
# set timestamp to get spool messages later
if @connectionEstablished
@lastSpoolMessage = Math.round( +new Date()/1000 )
@connectionEstablished = false
return if @backend is 'ajax'
# show error message, first try to reconnect
if !@error
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(
message: 'No connection to websocket, trying to reconnect...'
)
@ -197,5 +196,110 @@ class _Singleton extends App.Controller
@delay @connect, 4500
@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="span3 can-move">
<h2><%- @T( @head ) %></h2>
<% if @message: %>
<%- @T(@message) %>
<% end %>
<ul>
<% for item in @items: %>
<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
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
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])
if items == nil
render :json => { :message => "failed to fetch #{ params[:url] }", :status => :unprocessable_entity }
return
end
render :json => { :items => items }
end

View file

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

View file

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

View file

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

View file

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