Reputation: 453
I'm trying to implement javascript polling in my app but I'm running into a few problems. I'm pretty much following along with this railscasts. My problem is in trying to prepending any new data found. It prepends all of the data old and new and if there isn't any new data found it just prepends all of the old data. My other problem is that my setTimeout is only being called once, even after I try to keep it polling like they show in railscast. Below is my code. What am I doing wrong here?
polling.js
var InboxPoller;
InboxPoller = {
poll: function() {
return setTimeout(this.request, 5000);
},
request: function() {
return $.getScript($('.inbox_wrap').data('url'), {
after: function() {
$('.conversation').last().data('id')
}
});
}
};
$(function() {
if ($('.inbox_wrap').length > 0) {
return InboxPoller.poll();
}
});
polling.js.erb
$(".inbox_wrap").prepend("<%= escape_javascript(render @conversations, :locals => {:conversation => @conversation}) %>");
InboxPoller.poll();
conversations_controller.rb
class ConversationsController < ApplicationController
before_filter :authenticate_member!
helper_method :mailbox, :conversation
def index
@messages_count = current_member.mailbox.inbox({:read => false}).count
@conversations = current_member.mailbox.inbox.order('created_at desc').page(params[:page]).per_page(15)
end
def polling
@conversations = current_member.mailbox.inbox.where('conversation_id > ?', params[:after].to_i)
end
def show
@receipts = conversation.receipts_for(current_member).order('created_at desc').page(params[:page]).per_page(20)
render :action => :show
@receipts.mark_as_read
end
def create
recipient_emails = conversation_params(:recipients).split(',').take(14)
recipients = Member.where(user_name: recipient_emails).all
@conversation = current_member.send_message(recipients, *conversation_params(:body, :subject)).conversation
respond_to do |format|
format.html { redirect_to conversation_path(conversation) }
format.js
end
end
def reply
@receipts = conversation.receipts_for(current_member).order('created_at desc').page(params[:page]).per_page(20)
@receipt = current_member.reply_to_conversation(conversation, *message_params(:body, :subject))
respond_to do |format|
format.html { conversation_path(conversation) }
format.js
end
end
private
def mailbox
@mailbox ||= current_member.mailbox
end
def conversation
@conversation ||= mailbox.conversations.find(params[:id])
end
def conversation_params(*keys)
fetch_params(:conversation, *keys)
end
def message_params(*keys)
fetch_params(:message, *keys)
end
def fetch_params(key, *subkeys)
params[key].instance_eval do
case subkeys.size
when 0 then self
when 1 then self[subkeys.first]
else subkeys.map{|k| self[k] }
end
end
end
def check_current_subject_in_conversation
if !conversation.is_participant?(current_member)
redirect_to conversations_path
end
end
end
index.html.erb
<%= content_tag :div, class: "inbox_wrap", data: {url: polling_conversations_url} do %>
<%= render partial: "conversations/conversation", :collection => @conversations, :as => :conversation %>
<% end %>
_conversation.html.erb
<div id="conv_<%= conversation.id %>_<%= current_member.id %>" class="conversation" data-id="<%= conversation.id %>">
<div class="conv_body">
<%= conversation.last_message.body %>
</div>
<div class="conv_time">
<%= conversation.updated_at.localtime.strftime("%a, %m/%e %I:%M%P") %>
</div>
</div>
Upvotes: 2
Views: 3274
Reputation: 1954
Are you sure that polling is happening only once? Did you check in the console to make sure of that?
To me it looks like it might be happening, because I see issues in your javascript that prepends the content. What you have is this:
$(".inbox_wrap").prepend("<%= escape_javascript(render @conversations, :locals => {:conversation => @conversation}) %>");
This will actually insert the new content before inbox_wrap
div. I think your intention is to prepend to the conversations list. For that, you should change it to this (Reference jQuery Docs: http://api.jquery.com/prepend/):
$(".conversation").prepend("<%= escape_javascript(render @conversations, :locals => {:conversation => @conversation}) %>");
The next thing is that I am assuming you want to prepend the new conversations on top of the list, which means two things.
Assuming above is correct, you would need to get the first conversation's id in your InboxPoller
to pass to the controller as after
parameter.
$('.conversation').first().data('id')
One More Thing
Another thing is that you can use the native Javascript function setInterval
instead of setTimeout
. setInterval
executes a given function
periodically, as opposed to setTimeout
which does it only once. This way you won't have to initiate your InboxPoller
after every call to the back-end, in .js.erb
file.
Update
Again, looking at jQuery Documentation, it looks like $.getScript()
does not pass any parameters back to the server. Instead use $.get
as below:
InboxPoller = {
poll: function() {
return setInterval(this.request, 5000);
},
request: function() {
$.get($('.inbox_wrap').data('url'), {
after: function() {
return $('.conversation').first().data('id');
}
});
}
};
Also, I think you need to add .order('created_at desc')
in polling
method in your controller.
Upvotes: 0
Reputation: 76774
Javascript polling is extremely inefficient - basically sending requests every few seconds to your server to listen for "updates". Even then, in many cases, the updates will be entire files / batches of data with no succinctness
If we ever have to do something like this, we always look at using one of the more efficient technologies, specifically SSE
's or Websockets
--
SSE's
Have you considered using Server Sent Events
?
These are an HTML5 technology which work very similarly to the Javascript polling - sending requests every few seconds. The difference is the underlying way these work -- to listen to its own "channel" (mime type text/event-stream
-- allowing you to be really specific with the data you send)
You can call it like this:
#app/assets/javascript/application.js
var source = new EventSource("your/controller/endpoint");
source.onmessage = function(event) {
console.log(event.data);
};
This will allow you to create an endpoint
for the "event listener":
#config/routes.rb
resources :controller do
collection do
get :event_updates #-> domain.com/controller/event_updates
end
end
You can send the updates using the ActionController::Live::SSE
class:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
include ActionController::Live
def event_updates
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300, event: "event-name")
sse.write({ name: 'John'})
sse.write({ name: 'John'}, id: 10)
sse.write({ name: 'John'}, id: 10, event: "other-event")
sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
ensure
sse.close
end
end
--
Websockets
The preferred way to do this is to use websockets
Websockets are much more efficient than SSE's or standard JS polling, as they keep a connection open perpetually. This means you can send / receive any of the updates you require without having to send constant updates to the server
The problem with Websockets is the setup process - it's very difficult to run a WebSocket connection on your own app server, hence why many people don't do it.
If you're interested in Websockets, you may wish to look into using Pusher
- a third party websocket provider who have Ruby integration. We use this a lot - it's a very effective way to provide "real time" updates in your application, and no I'm not affiliated with them
Upvotes: 4