Ruby
Article

Realtime Mini-Chat with Rails and Faye

By Ilya Bodrov-Krukowski

Comments - Icon

In the previous part of this article, we built a messaging app that let users sign in via Facebook or Twitter and post their comments. We’ve also equipped the commenting form with AJAX so that the page is not reloaded every time the comment is posted.

However, there is a problem with our app – users have to reload the page each time they want to check for new comments. That is not very convenient, is it? How can we solve this problem?

There are at least two solutions: AJAX polling and web sockets. The first one is pretty outdated, however it can still be used in some scenarios where instant updates are not required. The latter seems to be more suitable for our case. However, let’s implement them both to see which is the best one!

The source code is available at GitHub.

The source code for the AJAX polling is available at the comments_polling branch.

The working demo can be found at http://sitepoint-minichat.herokuapp.com/.

AJAX Polling

I recommend switching to a new branch (if you are using Git) to start implementing this functionality:

$ git checkout -b polling

The key idea behind the AJAX polling is that we have some kind of timer that fires after a couple of seconds and sends a request checking if there are any new comments. The new comments are then displayed on the page. The cycle repeats.

There are some problems with this scheme, though. First of all, we are sending these poll requests constantly,
even if there are no new comments. Second, the new comments won’t appear on the page instantly – they will be displayed only on the next polling phase. There is also a third problem with polling that we will discuss a bit later.

To start our implemention, create a new index route to check for new comments:

config/routes.rb

[...]
resources :comments, only: [:new, :create, :index]
[...]

If you are using Turbolinks and haven’t included jquery-turbolinks gem yet, do it now, otherwise the default jQuery.ready() function will not work:

Gemfile

[...]
gem 'jquery-turbolinks'
[...]

application.js

[...]
//= require jquery.turbolinks
//= require turbolinks

Now create a new file .coffee file with the following code:

polling.coffee:

window.Poll = ->
  setTimeout ->
    $.get('/comments')
  , 5000

jQuery ->
  Poll() if $('#comments').size() > 0

Don’t forget to require this file:

application.js

[...]
//= require polling

We are creating a new function, Poll, on the window object (otherwise it will not be visible to the
other scripts). This function sets a timeout that fires after 5 seconds and sends a GET request to the /comments resource. Call this function as soon as DOM is loaded if there is a #comments block present on the page. We could have used setInterval instead, but if our server is responding slowly we may end up with a situation where we send requests too fast.

Now create an index method and the corresponding view:

comments_controller.rb

[...]
def index
  @comments = Comment.order('created_at DESC')
end
[...]

comments/index.js.erb

$('#comments').find('.media-list').html('<%= j render @comments %>');
window.Poll();

We are replacing all the comments with the new ones and once again set the timer to fire off. You are probably thinking that this is not an optimal solution, and you’re right. But for now, fire up the server, open your app in two separate windows, and try to post some comments. Your comments should appear in both windows.

Great, now you can chat with yourself when you feel bored!

Let’s optimize our solution and make the index action return only new comments, not all of them. One of the
ways to do this is to provide an id of the last comment available on the page. This way the resource will load only the comments with an id more than the provided one.

To implement this, we need to tweak our comment partial a bit:

comments/_comment.html.erb

<li class="media comment" data-id="<%= comment.id %>">
[...]

Now each comment has a data-id attribute storing its id. Also change the index method:

comments_controller.rb

[...]
def index
  @comments = Comment.where('id > ?', params[:after_id].to_i).order('created_at DESC')
end
[...]

The only thing we need to do now is send a GET request and provide an id of the last comment on the page. However, things start to get a bit complicated here.

Imagine this situation: User A and User B opened up our messaging app. User A posted his comment with an id of 50. User B then also posted a comment and it was assigned an id of 51. Now the polling phase occurs. On User A’s side everything is working fine – a GET request with an id of 50 is sent and therefore a new comment with an id of 51 is displayed.

User B, however, will not see User A’s comment because a GET request with an id of 51 will be sent and 50 is less that 51, therefore no new comments will be displayed for User B! To overcome this problem, we can forcibly initiate a polling phase after the comment is posted.

Let’s refactor our JS code a bit:

polling.coffee

window.Poller = {
  poll: (timeout) ->
    if timeout is 0
      Poller.request()
    else
      this.pollTimeout = setTimeout ->
        Poller.request()
      , timeout || 5000
  clear: -> clearTimeout(this.pollTimeout)
  request: ->
    first_id = $('.comment').first().data('id')
    $.get('/comments', after_id: first_id)
}

jQuery ->
  Poller.poll() if $('#comments').size() > 0

I’ve replaced Poll function with a Poller object. The interesting part here is the poll function. This function accepts a single argument – timeout. If timeout equals zero, initiate a poll request right away, without creating any timers. You may think that this condition is unneeded, because setTimeout(function() {}, 0); should fire instantly. However, this is not the case. In reality, there will still be a delay before the function is called, so we don’t need the setTimeout at all.

If timeout does not equal to zero, create a pollTimeout function and attach it to the Poller object. Note that we are providing a default value of 5000 for the timeout.

clear simply clears the pollTimeout.

The request is the actual GET request sent to /comments. It also provides an after_id parameter that equals the id of the newest (first) comment on the page.

We also need to tweak the index.js.erb view:

comments/index.js.erb

$('#comments').find('.media-list').prepend('<%= j render @comments %>');
Poller.poll();

We’ve change the html method to prepend and Poll() function to Poller.poll().

Lastly, we also need to change the create.js.erb:

comments/create.js.erb

Poller.clear();
Poller.poll(0);
<% if @comment.new_record? %>
alert('Your comment cannot be saved.');
<% else %>
$('#comment_body').val('');
<% end %>

As soon as the comment is posted, clear the currently set timer and initiate the polling phase. Also, note that we do not prepend the comment here – we are relying on index.js.erb. to do that.

Now this is a pretty messy, isn’t it?

To check if this is working correctly open up your app in two different windows, post a comment in the first window and then quickly (before the polling phase occurs) post another comment in the second window. You should see both comments displayed in the correct order.

AJAX polling is now working correctly. However, it does not seem to be the best solution. Will web sockets save the day?

Web Sockets and Faye

In this last iteration, we will use Faye – a publish/subscribe messaging system available for Ruby and Node.js. To integrate it with Rails, the faye-rails gem by James Coglan will come in handy.

If in the previous iteration we polled our app on a regular time basis to check for new comments, here we instead subscribe for updates and take any action only if an update (comment) is published. When a comment is created, notify all the subscribers so the update process happens instantly.

Switch back to the master branch and start off by adding two new gems to the Gemfile

Gemfile

[...]
gem 'faye-rails', '~> 2.0`
gem 'thin'

and installing it

$ bundle install

We have to switch to Thin web server, because Faye will not work with the WEBrick. If you are going to publish your app on Heroku, create a new file in the root of your project:

Procfile

web: bundle exec rails server thin -p $PORT -e $RACK_ENV

Now add these lines to the application.rb:

config/application.rb

[...]
config.middleware.delete Rack::Lock
config.middleware.use FayeRails::Middleware, mount: '/faye', :timeout => 25
[...]

We are using FayeRails as middleware and mounting it on the /faye path.

Now, require two new files in the application.js:

application.js

[...]
//= require faye
//= require comments

The faye.js file is provided by faye-rails. It contains all the required JS code for Faye to work correctly.

Create the comments.coffee file:

comments.coffee

window.client = new Faye.Client('/faye')

jQuery ->
  client.subscribe '/comments', (payload) ->
    $('#comments').find('.media-list').prepend(payload.message) if payload.message

We are creating a new Faye client and attaching it to the window object. Subscribe this client to the /comments channel. As soon as the update is received, prepend the new message to the list of comments. This is much easier than the AJAX polling, isn’t it?

The last thing to do is modify the create.js.erb view:

comments/create.js.erb

publisher = client.publish('/comments', {
  message: '<%= j render @comment %>'
});

Here the client is used to publish an update to the /comments channel providing the message that was posted. Note that we do not need to use the prepend method here because it is used in our comments.coffee file.

Fire up the server and, again, chat with yourself (I hope you are not getting depressed). Notice that the messages are appearing instantly, in contrast to the AJAX polling. Moreover, the code is easier and shorter.

The last thing we could do is disable the “Post” button while the comment is being sent:

comments.coffee

[...]
jQuery ->
  client.subscribe '/comments', (payload) ->
    $('#comments').find('.media-list').prepend(payload.message) if payload.message

  $('#new_comment').submit -> $(this).find("input[type='submit']").val('Sending...').prop('disabled', true)

The prop method is used to set the disabled attribute on the input to true.

With the help of Faye callbacks, we can restore the button to its original state and clear the textarea as soon as the comment is posted. If there was an error while notifying the subscribers, display it as well:

comments/create.js.erb

publisher = client.publish('/comments', {
  message: '<%= j render @comment %>'
});

publisher.callback(function() {
  $('#comment_body').val('');
  $('#new_comment').find("input[type='submit']").val('Submit').prop('disabled', false)
});

publisher.errback(function() {
  alert('There was an error while posting your comment.');
});

How cool is that? With web sockets, our app feels like a real chat. Messages are being sent with a minimum delay and no page refresh is needed! Of course, this app can still be extended with a backend for admin users allowing them to delete comments or block misbehaving chatters.

You may also want to take a look at the Faye-rails demo app – a very simple chat without any database or authentication.

Security

I have to say a couple of words about security. Currently anyone can send any data to the /comments channel, which is not very secure. Faye’s website has a Security section that describes how to restrict subscriptions, publications, enforce CSRF protection, and some other concepts. I encourage you to read it if you are going to implement Faye in a real app.

For this demo, we are going to enforce CSRF protection – this will require us to modify both the server-side and client-side code. The easiest way to achieve this task is using so-called Faye extensions:

config/application.rb

require File.expand_path('../boot', __FILE__)
require File.expand_path('../csrf_protection', __FILE__)

[...]

class Application < Rails::Application
  config.middleware.use FayeRails::Middleware, extensions: [CsrfProtection.new], mount: '/faye', :timeout => 25
[...]

config/csrf_protection.rb

class CsrfProtection
  def incoming(message, request, callback)
    session_token = request.session['_csrf_token']
    message_token = message['ext'] && message['ext'].delete('csrfToken')

    unless session_token == message_token
      message['error'] = '401::Access denied'
    end
    callback.call(message)
  end
end

We are checking if _csrf_token is set in the user’s session and compare it with the token sent alongside with the message. If they are not the same, we raise an “Access denied” error.

On to the client-side code:

comments.coffee

window.client = new Faye.Client('/faye')

client.addExtension {
  outgoing: (message, callback) ->
    message.ext = message.ext || {}
    message.ext.csrfToken = $('meta[name=csrf-token]').attr('content')
    callback(message)
}

Using the addExtension method, store the csrfToken inside the ext attribute of the message. This way, all publish messages will include the CSRF token. If you remove this client.addExtension line, open your app along with the javascript console. The browser tries to establish connection but fails because on we require CSRF token to be present.

Conclusion

We’ve come to the end of this two-part article. All the planned functionality was implemented and we prototype AJAX polling vs web sockets, confirming what we already knew: Web sockets are much better for a real chat experience.

Have you ever implemented a similar solution? What problems did you face? Share your experience in the comments!

Thanks for reading!

============================
Update (08.12.2014):

One of the readers have noticed that when you navigate to the chat, then to another page, and return to the chat all sent messages will be duplicated. This happens because each time you visit a page the client subscribes to the `/comments` channel once again, so when you post a message it is being broadcasted via all subscriptions. To fix this problem you can just try to unsubscribe before subscribing. Add the following piece of code to your comments.coffee file:

try
 client.unsubscribe '/comments'
catch
 console?.log "Can't unsubscribe." # print a message only if console is defined
end

just before the

client.subscribe '/comments', (payload) ->

line. See this diff on GitHub:

Thank you guys so much for all your feedback!
============================

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • Gee Bee

    Very nice series.
    Would love to see implementation using Server Side Events, which seems to be well suited for new comments notifications.

    • Ilya Bodrov

      Glad you’ve liked it! Well, in the following couple of weeks my two other articles will be publishes; after that I will try to research this technology.

  • Mostafa hosny

    it works well with Ajax but having a problem with Faye i have to refresh the page to see the comment updated ?
    what is the problem ?

    • Ilya Bodrov

      Someting is definetely wrong. Any errors in your console? You should see all the comments instantly.

      • Mostafa hosny

        it’s worked but didn’t work when i push in heruoku

        • Ilya Bodrov

          Well, there might be a handful of cases. Have your assets precompiled with no problems? If no then Faye’s js file will not be hooked and therefore Faye will not work. You might check that out by looking at the page source and specifically trying to open the compiled JS file in your head. If the 404 error is returned – there is something with your assets. Maybe there are any errors in the browser’s console when opening the page?

          • Mostafa hosny

            now it’s work give me this message on console
            2014-11-08T12:01:45.110418+00:00 app[web.1]: Parameters: {“utf8″=>”✓”, “message”=>{“body”=>”heelo”}, “commit”=>”Send”, “trip_id”=>”1″}
            2014-11-08T12:01:45.173832+00:00 app[web.1]: Rendered messages/_message.html.erb (4.5ms)
            2014-11-08T12:01:45.173999+00:00 app[web.1]: Rendered messages/create.js.erb (8.4ms)
            2014-11-08T12:01:45.174238+00:00 app[web.1]: Completed 200 OK in 64ms (Views: 13.4ms | ActiveRecord: 25.3ms)
            2014-11-08T12:01:45.179291+00:00 heroku[router]: at=info method=POST path=”/trips/1/messages” host=raye77.herokuapp.com request_id=914ba743-d15e-47b3-8acd-416558623f32 fwd=”197.162.143.244″ dyno=web.1 connect=1ms service=78ms status=200 bytes=2554

            but i must refresh the page to see the message and delete the text_area
            ?

  • Mostafa hosny

    why you used form_for @comment and didn’t use :comment ?

  • Ilya Bodrov

    Well judging by this Rendered messages/create.js.erb the create.js.erb was rendered. Maybe there are some problems in that file? Do you see errors in the browser’s console (firebug)?

    • Mostafa hosny

      yes there is a demo on this link
      https://raye77.herokuapp.com/trips/1/messages?

      • Ilya Bodrov

        There is something with Faye. If your open Firebug and navigate to the demo project for this article you’ll notice this line: “POST http://sitepoint-minichat.herokuapp.com/faye“. However it does not appear on your page. So seems like problems with the JS code. Unfortunately without seeing the actual code I can’t tell what is the problem.

  • Ilya Bodrov

    Is there a website where I can reproduce that?

  • Mostafa hosny

    can you give me your email please ? for contact

    • Ilya Bodrov

      Yeah, check out my website (radiant-wind.com). I’ve listed some ways to contact me there :)

  • http://www.seoyoochan.com/ Yoochan Seo

    Just one quick question.
    Do you think I can apply the AJAX Polling functionality for updating comments in my real app?
    I personally do not want to provide a subscrption button to users for notifying new comments.
    My purpose is to automatically display a ‘update’ button when new comments were created to users who are currently viewing a post.

    I will use web sockets and Faye definitely to chat system though.
    What is your thought on this?

    • Ilya Bodrov

      Well, it could be used but the described problems will appear (I presume that you are going to publish user’s own comments immediately with AJAX)

  • hihihaha

    If you click on the link Mini Chat, messages that you send will be duplicated. Can you please tell how to fix it?

    • Ilya Bodrov

      That is strange, I can’t reproduce this. Is this thing happening in demo app?

  • http://citrusfortress.com/ tonyrobots

    Love this series. It’s very clear and well-written. I’ve implemented a web sockets chat for a game I’m working on, and now I’d also like to have certain server events push messages to a channel as well. Do you have any suggestions on how one might publish a comment from the server side that would immediately appear in all clients? I have a hacky solution whereby I’m setting a gon.message variable in the controller, and then looking for it in the javascript. If present, it sends the message to the channel. But that then doesn’t have the formatting from the _comment partial. Would love to hear your thoughts!

    • http://citrusfortress.com/ tonyrobots

      I’ve managed to do it nicely. First off, I’ve created a couple of methods in my application_controller.rb:

      def add_message(user, msg)
      @comment = Comment.new(body:msg,user_id:user.id)
      @comment.save
      channel = “/comments”

      payload = { message: render_to_string(@comment)}
      broadcast(channel, payload)
      end

      def broadcast(channel, payload)
      base_url = request ? request.base_url : “http://localhost:7777”
      client = Faye::Client.new(“#{base_url}/faye”)
      client.publish(channel, payload )
      end

      That’s it! Now I can call add_message from anywhere and the message gets added to the comments table, and broadcast to all clients subscribed to the “/comments” channel.

  • http://www.seoyoochan.com/ Yoochan Seo

    I don’t know why the heck it threw the error, but everything works well now. ;)
    I always like your tutorials, which mean is I guess I am a fan of you now?:D

    • Ilya Bodrov

      Cool!

      Lol, I don’t know:) I’m doing this for you guys and it is great that you find my work useful!

      • http://www.seoyoochan.com/ Yoochan Seo

        Another advanced question for you. I am trying to have to use real-time notifications, chat, image-resizing/crop functionality, posts, and comments for any client, which inlcudes a web browser, mobile devices, and desktop applications.

        I was researching how to implement this with what combinations of technologies.
        (https://www.seoyoochan.com/blog)

        One disadvantage of Faye that I heard of was that it was hard to implement for mobile clients due to Bayeux’s bad documentation. Although Faye handles lots of requests like Node.js does on web service.

        I was thinking of using Redis & Resque (but one of slideshare presentations said developers have to write their own worker in Ruby)
        “Writing a worker is easy, but writing a long-running-and-stable worker is hard ” said by the presentor.

        I was thinking of RabbitMQ or Bunny as well.

        I really do not know how to approach for the best stack of my service.

        I also want to serve my static assets on another server.
        Should I use master-slave mysql db then? I already use AWS bucket for uploading files as a storage.

        Sorry. I am neither a S.E or a System Administrator.
        Could you give me a advice?

        • Ilya Bodrov

          I have not forgotten about this one but unfortuntely had no possibility to research the topic. Regarding the statis assets – we’ve used `config.action_controller.asset_host` to point to other server used as CDN and store assets there. I believe you can also use AWS as well.

          I’ve worked with RabbitMQ a bit and can say it’s pretty complex as well, not sure about Bayeux.

          There is an open source project (I am contributing to it sometimes) https://github.com/kandanapp/kandan that revolves around building a chat app with the possibility to upload files. Of course that’s much simpler than your case but maybe you would be able to carry out something of the source code. Backbone.js is used there as a client-side framework btw.

  • Ilya Bodrov

    Sorry for the late response, I’ve just seen your message. Seems like a nice solution!

  • Ilya Bodrov

    I will try to think of something and write you back.

  • Tony

    How to deploy all this to DigitalOcean?

  • Dennis

    I just released a gem that would pretty much allow you to automate these things by using Redis rather than Faye: https://github.com/so-entangled/rails

    Give it a try!

  • xnjiang

    Hi I am on rails 4.2.1 It seems meta[name=csrf-token] not match request.session[‘_csrf_token’]

  • Carmelo Buscemi

    Hi, sorry for the disturb.
    You found a solution for that problem? Because I’ve exactly the same. Need to refresh the page to update it.

    • Ilya Bodrov

      No worries! I gave Mostafa some advices however ultimately I don’t know how did he solve the problem – what I remember is that he had some issues with the CoffeeScript code. If you keep getting this error, please contact me via e-mail while providing the relevant code, I’ll try to help.

  • kpagcha

    getting this error: “ReferenceError: Faye” is not defined in the first Faye implementation

  • Heru Oktafian ST

    can I get the capture screen of the result the application ?

  • Matteo Lashinsky

    Perhaps a stupid question (I am a n00b), but in order to get the Websocket portion of the application working, building out the previous part (http://www.sitepoint.com/mini-chat-rails/) is required as well, correct? Seems this section is lacking views/model/etc

  • http://girlxinhvd.com/ girlxinhvd.com

    Can you help me ? I load some comments for my app, but i have a problem : [{“id”:”71″,”channel”:”/meta/handshake”,”error”:”401::Access denied”,”successful”:false,”version”:”1.0″,”supportedConnectionTypes”:[“long-polling”,”cross-origin-long-polling”,”callback-polling”,”websocket”,”eventsource”,”in-process”],”advice”:{“reconnect”:”handshake”}}]. Thanks

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.