Fixed issue #2327 - Websocket messages are not working correctly via Ajax long polling (e. g. multiple browser tabs can get opened with one session).
This commit is contained in:
parent
764d262cff
commit
75230c3db3
15 changed files with 303 additions and 51 deletions
|
@ -35,6 +35,11 @@ class App.WebSocket
|
|||
_instance ?= new _webSocketSingleton
|
||||
_instance.support()
|
||||
|
||||
@queue: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _webSocketSingleton
|
||||
_instance.queue
|
||||
|
||||
# The actual Singleton class
|
||||
class _webSocketSingleton extends App.Controller
|
||||
@include App.LogInclude
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class LongPollingController < ApplicationController
|
||||
skip_before_action :session_update # prevent race conditions
|
||||
prepend_before_action :authentication_check_only
|
||||
|
||||
# GET /api/v1/message_send
|
||||
def message_send
|
||||
|
@ -14,24 +15,22 @@ class LongPollingController < ApplicationController
|
|||
client_id = client_id_gen
|
||||
log 'new client connection', client_id
|
||||
end
|
||||
if !params['data']
|
||||
params['data'] = {}
|
||||
end
|
||||
data = params['data'].permit!.to_h
|
||||
session_data = {}
|
||||
if current_user&.id
|
||||
session_data = { 'id' => current_user.id }
|
||||
end
|
||||
|
||||
# spool messages for new connects
|
||||
if params['data']['spool']
|
||||
Sessions.spool_create(params['data'])
|
||||
if data['spool']
|
||||
Sessions.spool_create(data)
|
||||
end
|
||||
if params['data']['event'] == 'login'
|
||||
if data['event'] == 'login'
|
||||
Sessions.create(client_id, session_data, { type: 'ajax' })
|
||||
elsif params['data']['event']
|
||||
elsif data['event']
|
||||
message = Sessions::Event.run(
|
||||
event: params['data']['event'],
|
||||
payload: params['data'],
|
||||
event: data['event'],
|
||||
payload: data,
|
||||
session: session_data,
|
||||
client_id: client_id,
|
||||
clients: {},
|
||||
|
@ -41,15 +40,15 @@ class LongPollingController < ApplicationController
|
|||
Sessions.send(client_id, message)
|
||||
end
|
||||
else
|
||||
log "unknown message '#{params['data'].inspect}'", client_id
|
||||
log "unknown message '#{data.inspect}'", client_id
|
||||
end
|
||||
|
||||
if new_connection
|
||||
result = { client_id: client_id }
|
||||
render json: result
|
||||
else
|
||||
render json: {}
|
||||
return
|
||||
end
|
||||
render json: {}
|
||||
end
|
||||
|
||||
# GET /api/v1/message_receive
|
||||
|
|
|
@ -334,6 +334,15 @@ send message to recipient client
|
|||
|
||||
Sessions.send_to(user_id, data)
|
||||
|
||||
e. g.
|
||||
|
||||
Sessions.send_to(user_id, {
|
||||
event: 'session:takeover',
|
||||
data: {
|
||||
taskbar_id: 12312
|
||||
},
|
||||
})
|
||||
|
||||
returns
|
||||
|
||||
true|false
|
||||
|
@ -476,6 +485,14 @@ remove all session and spool messages
|
|||
true
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
create spool messages
|
||||
|
||||
Sessions.spool_create(some: 'data')
|
||||
|
||||
=end
|
||||
|
||||
def self.spool_create(data)
|
||||
msg = JSON.generate(data)
|
||||
path = "#{@path}/spool/"
|
||||
|
@ -492,6 +509,14 @@ remove all session and spool messages
|
|||
end
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
get spool messages
|
||||
|
||||
Sessions.spool_list(junger_then, for_user_id)
|
||||
|
||||
=end
|
||||
|
||||
def self.spool_list(timestamp, current_user_id)
|
||||
path = "#{@path}/spool/"
|
||||
FileUtils.mkpath path
|
||||
|
@ -512,6 +537,7 @@ remove all session and spool messages
|
|||
file.flock(File::LOCK_SH)
|
||||
message = file.read
|
||||
file.flock(File::LOCK_UN)
|
||||
message_parsed = {}
|
||||
begin
|
||||
spool = JSON.parse(message)
|
||||
message_parsed = JSON.parse(spool['msg'])
|
||||
|
@ -530,7 +556,7 @@ remove all session and spool messages
|
|||
# add spool attribute to push spool info to clients
|
||||
message_parsed['spool'] = true
|
||||
|
||||
# only send not already now messages
|
||||
# only send not already older messages
|
||||
if !timestamp || timestamp < spool['timestamp']
|
||||
|
||||
# spool to recipient list
|
||||
|
@ -573,6 +599,19 @@ remove all session and spool messages
|
|||
data
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
delete spool messages
|
||||
|
||||
Sessions.spool_delete
|
||||
|
||||
=end
|
||||
|
||||
def self.spool_delete
|
||||
path = "#{@path}/spool/"
|
||||
FileUtils.rm_rf path
|
||||
end
|
||||
|
||||
def self.jobs(node_id = nil)
|
||||
|
||||
# just make sure that spool path exists
|
||||
|
|
|
@ -7,7 +7,7 @@ class Sessions::Event
|
|||
begin
|
||||
backend = load_adapter(adapter)
|
||||
rescue => e
|
||||
return { event: 'error', data: { error: "No such event #{params[:event]}", payload: params[:payload] } }
|
||||
return { event: 'error', data: { error: "No such event #{params[:event]}: #{e.inspect}", payload: params[:payload] } }
|
||||
end
|
||||
|
||||
begin
|
||||
|
|
|
@ -9,6 +9,10 @@ class Sessions::Event::Base
|
|||
return if !@clients[@client_id]
|
||||
|
||||
@is_web_socket = true
|
||||
|
||||
return if !self.class.instance_variable_get(:@database_connection)
|
||||
|
||||
ActiveRecord::Base.establish_connection
|
||||
end
|
||||
|
||||
def websocket_send(recipient_client_id, data)
|
||||
|
@ -120,8 +124,18 @@ class Sessions::Event::Base
|
|||
puts "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}"
|
||||
#puts "#{Time.now.utc.iso8601}:#{ level }:client(#{ client_id }) #{ data }"
|
||||
# rubocop:enable Rails/Output
|
||||
#Rails.logger.info "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}"
|
||||
end
|
||||
|
||||
def destroy; end
|
||||
def self.database_connection_required
|
||||
@database_connection = true
|
||||
end
|
||||
|
||||
def destroy
|
||||
return if !@is_web_socket
|
||||
return if !self.class.instance_variable_get(:@database_connection)
|
||||
|
||||
ActiveRecord::Base.remove_connection
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
class Sessions::Event::Broadcast < Sessions::Event::Base
|
||||
|
||||
=begin
|
||||
|
||||
Event module to broadcast messages to all client connections.
|
||||
|
||||
To execute this manually, just paste the following into the browser console
|
||||
|
||||
App.WebSocket.send({event:'broadcast', recipient: { user_id: [1,2,3]}, data: {some: 'key'}})
|
||||
|
||||
=end
|
||||
|
||||
def run
|
||||
|
||||
# list all current clients
|
||||
|
|
|
@ -1,17 +1,5 @@
|
|||
class Sessions::Event::ChatBase < Sessions::Event::Base
|
||||
|
||||
def initialize(params)
|
||||
super(params)
|
||||
return if !@is_web_socket
|
||||
|
||||
ActiveRecord::Base.establish_connection
|
||||
end
|
||||
|
||||
def destroy
|
||||
return if !@is_web_socket
|
||||
|
||||
ActiveRecord::Base.remove_connection
|
||||
end
|
||||
database_connection_required
|
||||
|
||||
def run
|
||||
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
class Sessions::Event::Login < Sessions::Event::Base
|
||||
database_connection_required
|
||||
|
||||
=begin
|
||||
|
||||
Event module to start websocket session for new client connections.
|
||||
|
||||
To execute this manually, just paste the following into the browser console
|
||||
|
||||
App.WebSocket.send({event:'login', session_id: '123'})
|
||||
|
||||
=end
|
||||
|
||||
def run
|
||||
|
||||
# get user_id
|
||||
session = nil
|
||||
if @is_web_socket
|
||||
ActiveRecord::Base.establish_connection
|
||||
end
|
||||
|
||||
app_version = AppVersion.event_data
|
||||
|
||||
|
@ -14,12 +22,7 @@ class Sessions::Event::Login < Sessions::Event::Base
|
|||
session = ActiveRecord::SessionStore::Session.find_by(session_id: @payload['session_id'])
|
||||
end
|
||||
|
||||
if @is_web_socket
|
||||
ActiveRecord::Base.remove_connection
|
||||
end
|
||||
|
||||
new_session_data = {}
|
||||
|
||||
if session&.data && session.data['user_id']
|
||||
new_session_data = {
|
||||
'id' => session.data['user_id'],
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
class Sessions::Event::Maintenance < Sessions::Event::Base
|
||||
database_connection_required
|
||||
|
||||
def initialize(params)
|
||||
super(params)
|
||||
return if !@is_web_socket
|
||||
=begin
|
||||
|
||||
ActiveRecord::Base.establish_connection
|
||||
end
|
||||
Event module to broadcast maintenance messages to all client connections.
|
||||
|
||||
def destroy
|
||||
return if !@is_web_socket
|
||||
To execute this manually, just paste the following into the browser console
|
||||
|
||||
ActiveRecord::Base.remove_connection
|
||||
end
|
||||
App.WebSocket.send({event:'maintenance', data: {some: 'key'}})
|
||||
|
||||
=end
|
||||
|
||||
def run
|
||||
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
class Sessions::Event::Ping < Sessions::Event::Base
|
||||
|
||||
=begin
|
||||
|
||||
Event module to send pong to client connection.
|
||||
|
||||
To execute this manually, just paste the following into the browser console
|
||||
|
||||
App.WebSocket.send({event:'ping'})
|
||||
|
||||
=end
|
||||
|
||||
def run
|
||||
{
|
||||
event: 'pong',
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
class Sessions::Event::Spool < Sessions::Event::Base
|
||||
|
||||
# get spool messages and send them to new client connection
|
||||
=begin
|
||||
|
||||
Event module to serve spool messages and send them to new client connection.
|
||||
|
||||
To execute this manually, just paste the following into the browser console
|
||||
|
||||
App.WebSocket.send({event:'spool'})
|
||||
|
||||
=end
|
||||
|
||||
def run
|
||||
|
||||
# error handling
|
||||
|
@ -11,8 +20,13 @@ class Sessions::Event::Spool < Sessions::Event::Base
|
|||
end
|
||||
|
||||
if !@session || !@session['id']
|
||||
log 'error', "can't send spool, session not authenticated"
|
||||
return
|
||||
log 'error', "Can't send spool, session not authenticated"
|
||||
return {
|
||||
event: 'error',
|
||||
data: {
|
||||
error: 'Can\'t send spool, session not authenticated',
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
spool = Sessions.spool_list(@payload['timestamp'], @session['id'])
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Sessions::Event::TicketOverviewIndex < Sessions::Event::Base
|
||||
database_connection_required
|
||||
|
||||
def run
|
||||
return if !valid_session?
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Sessions::Event::TicketOverviewList < Sessions::Event::Base
|
||||
database_connection_required
|
||||
|
||||
def run
|
||||
return if !valid_session?
|
||||
|
|
46
lib/sessions/event/who_am_i.rb
Normal file
46
lib/sessions/event/who_am_i.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
class Sessions::Event::WhoAmI < Sessions::Event::Base
|
||||
database_connection_required
|
||||
|
||||
=begin
|
||||
|
||||
Event module to send `who am i` to client connection.
|
||||
|
||||
To execute this manually, just paste the following into the browser console
|
||||
|
||||
App.WebSocket.send({event:'who_am_i'})
|
||||
|
||||
=end
|
||||
|
||||
def run
|
||||
|
||||
if !@session || !@session['id']
|
||||
return {
|
||||
event: 'who_am_i',
|
||||
data: {
|
||||
message: 'session not authenticated',
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
user = User.find_by(id: @session['id'])
|
||||
|
||||
if !user
|
||||
return {
|
||||
event: 'who_am_i',
|
||||
data: {
|
||||
message: "No such user with id #{@session['id']}",
|
||||
},
|
||||
}
|
||||
end
|
||||
attributes = user.attributes
|
||||
attributes.delete('password')
|
||||
{
|
||||
event: 'who_am_i',
|
||||
data: {
|
||||
message: 'session authenticated',
|
||||
user: attributes,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
end
|
124
spec/requests/long_polling_spec.rb
Normal file
124
spec/requests/long_polling_spec.rb
Normal file
|
@ -0,0 +1,124 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'LongPolling', type: :request do
|
||||
|
||||
let(:agent_user) do
|
||||
create(:agent_user)
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
Sessions.sessions.each do |client_id|
|
||||
Sessions.destroy(client_id)
|
||||
end
|
||||
Sessions.spool_delete
|
||||
end
|
||||
|
||||
describe 'request handling' do
|
||||
|
||||
it 'receive without client_id - no user login' do
|
||||
get '/api/v1/message_receive', params: { data: {} }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('Invalid client_id receive!')
|
||||
end
|
||||
|
||||
it 'send without client_id - no user login' do
|
||||
get '/api/v1/message_send', params: { data: {} }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
|
||||
|
||||
client_id = json_response['client_id']
|
||||
get '/api/v1/message_send', params: { client_id: client_id, data: { event: 'spool' } }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
|
||||
|
||||
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('Invalid client_id receive!')
|
||||
end
|
||||
|
||||
it 'receive without client_id' do
|
||||
authenticated_as(agent_user)
|
||||
get '/api/v1/message_receive', params: { data: {} }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('Invalid client_id receive!')
|
||||
end
|
||||
|
||||
it 'receive without wrong client_id' do
|
||||
authenticated_as(agent_user)
|
||||
get '/api/v1/message_receive', params: { client_id: 'not existing', data: {} }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('Invalid client_id receive!')
|
||||
end
|
||||
|
||||
it 'send without client_id' do
|
||||
authenticated_as(agent_user)
|
||||
get '/api/v1/message_send', params: { data: {} }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
|
||||
end
|
||||
|
||||
it 'send with client_id' do
|
||||
Sessions.create('123456', {}, { type: 'ajax' })
|
||||
authenticated_as(agent_user)
|
||||
get '/api/v1/message_send', params: { client_id: '123456', data: {} }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).to eq({})
|
||||
end
|
||||
|
||||
it 'send event spool and receive data' do
|
||||
|
||||
authenticated_as(agent_user)
|
||||
get '/api/v1/message_send', params: { data: { event: 'login' } }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
|
||||
client_id = json_response['client_id']
|
||||
|
||||
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Array)
|
||||
expect(json_response).to eq([{ 'data' => { 'success' => true }, 'event' => 'ws:login' }])
|
||||
|
||||
get '/api/v1/message_send', params: { client_id: client_id, data: { event: 'spool' } }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).to eq({})
|
||||
|
||||
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Array)
|
||||
expect(json_response[0]['event']).to eq('spool:sent')
|
||||
expect(json_response[0]['event']).to eq('spool:sent')
|
||||
expect(json_response.count).to eq(1)
|
||||
|
||||
spool_list = Sessions.spool_list(Time.now.utc.to_i, agent_user.id)
|
||||
expect(spool_list).to eq([])
|
||||
|
||||
get '/api/v1/message_send', params: { client_id: client_id, data: { event: 'broadcast', spool: true, recipient: { user_id: [agent_user.id] }, data: { taskbar_id: 9_391_633 } } }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).to eq({})
|
||||
|
||||
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).to eq({ 'event' => 'pong' })
|
||||
|
||||
travel 2.seconds
|
||||
|
||||
spool_list = Sessions.spool_list(Time.now.utc.to_i, agent_user.id)
|
||||
expect(spool_list).to eq([])
|
||||
|
||||
spool_list = Sessions.spool_list(nil, agent_user.id)
|
||||
expect(spool_list).to eq([{ message: { 'taskbar_id' => 9_391_633 }, type: 'direct' }])
|
||||
end
|
||||
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue