2

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>
1

2 Answers 2

4

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

3
  • Yes I have looked at Pusher and websockets and have thought about going that route but at this early stage didn't really need or want to go that route. And from my understanding(I may be wrong), but the code would be identical to the code I'm trying to use for polling so getting this code to work properly I could make the switch fairly easy.
    – iamdhunt
    Commented Jul 25, 2014 at 11:21
  • 2
    Polling has the advantage of being extremely simple and for many cases simplicity trumps complexity. Commented May 20, 2016 at 12:39
  • i think rails has action cable which uses websockets, it probably didn't have that when you wrote your answer
    – barlop
    Commented Apr 8, 2019 at 23:58
0

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.

  1. The controller will return new conversations ordered by date in descending order.
  2. 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.

4
  • I tried to make those changes, but the problem still occurs. Must be something else going on. And I know setTimeout isn't firing again because even if I call something like alert it only alerts once. I switched it over to setInterval though and it's initiating again.
    – iamdhunt
    Commented Jul 25, 2014 at 22:28
  • Hm it still just keeps updating the list with all of the data, when I only want it to update it with any new data found. So if there is no new data it just prepends the list with all of the data. It's like it's ignoring :after
    – iamdhunt
    Commented Jul 25, 2014 at 23:37
  • This is the first time you have mentioned that some update IS happening. Your question is titled "JavaScript polling" and we can agree that that problem is solved. I think you should create a new question about why correct data is not showing up. I can't really understand your models like what is the relationship between conversation and inbox etc.
    – San
    Commented Jul 26, 2014 at 0:09
  • Sorry if I wasn't more clear but I did say in my question " 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." Thanks for the help anyways.
    – iamdhunt
    Commented Jul 26, 2014 at 0:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.