Key Takeaways
- Utilize AJAX polling or Web Sockets with Faye to create a real-time chat application in Rails, overcoming the need for manual page refreshes.
- Implement AJAX polling by setting timers to regularly check for new comments, though this method introduces a slight delay in displaying comments and increases server load.
- Opt for the Faye gem and Web Sockets for instant, real-time updates without the need for constant server requests, providing a smoother user experience.
- Secure your chat application by implementing CSRF protection and using Faye extensions to manage security concerns effectively.
- Enhance user interaction by disabling the “Post” button while comments are being sent, using Faye callbacks to update UI elements dynamically.
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!
============================
Frequently Asked Questions about Realtime Mini-Chat with Rails and Faye
How do I install Faye on Rails?
To install Faye on Rails, you need to add the Faye gem to your Gemfile. Open your Gemfile and add the following line: gem 'faye-rails', '~> 2.0'
. Then, run bundle install
in your terminal to install the gem. After the gem is installed, you need to mount the Faye server in your Rails application. You can do this by adding the following lines to your config/routes.rb
file: mount Faye::Rails::Application.new(Rails.application.config.faye), at: '/faye'
.
How do I use Faye to create a real-time chat application?
Faye is a publish-subscribe messaging system that allows you to create real-time applications. To create a chat application, you need to create a new channel for each chat room. When a user sends a message, you publish it to the corresponding channel. Other users subscribed to the same channel will receive the message in real-time. You can use the Faye::Client#publish
method to send messages and the Faye::Client#subscribe
method to receive messages.
What is the difference between Faye and other real-time messaging systems?
Faye is a simple and flexible messaging system that can be used to create a variety of real-time applications. Unlike other messaging systems, Faye is not tied to a specific application or technology. It can be used with any web server and any client that supports WebSocket or long-polling. This makes Faye a versatile choice for developers who want to create real-time applications.
How do I handle errors in Faye?
Faye provides several methods for handling errors. The Faye::Client#on
method allows you to listen for error events. When an error occurs, Faye will emit an ‘error’ event with the error message as the argument. You can use this method to log errors or display error messages to the user.
How do I secure my Faye chat application?
Faye provides several mechanisms for securing your chat application. You can use the Faye::Authentication
module to authenticate users and control access to channels. You can also use SSL/TLS to encrypt the communication between the client and the server. Additionally, you can use the Faye::RackAdapter
to integrate Faye with your existing Rack middleware stack, allowing you to use any Rack-compatible security middleware.
How do I scale my Faye chat application?
Faye is designed to be scalable and can handle a large number of concurrent connections. You can scale your Faye chat application by adding more Faye servers and using a load balancer to distribute the load among them. Faye also supports clustering, which allows you to run multiple Faye servers on different machines and synchronize their state.
How do I test my Faye chat application?
You can test your Faye chat application using any testing framework that supports WebSocket or long-polling. You can use the Faye::Client
class to simulate clients and send and receive messages. You can also use the Faye::Server
class to simulate a Faye server and test your server-side code.
How do I debug my Faye chat application?
Faye provides several methods for debugging your chat application. The Faye::Client#on
method allows you to listen for various events, such as ‘transport:down’ and ‘transport:up’, which can help you diagnose connectivity issues. You can also use the Faye::Logging
module to log all Faye operations.
How do I integrate Faye with other Rails components?
Faye can be easily integrated with other Rails components. You can use the Faye::RackAdapter
to integrate Faye with your existing Rack middleware stack. You can also use the Faye::Rails::Controller
class to integrate Faye with your Rails controllers and views.
How do I deploy my Faye chat application?
You can deploy your Faye chat application like any other Rails application. You need to ensure that your deployment environment supports WebSocket or long-polling. You also need to configure your web server to proxy requests to the Faye server. If you are using a cloud hosting provider, you may need to use a WebSocket-compatible load balancer or reverse proxy.
Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.