Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save haslinger/8956933 to your computer and use it in GitHub Desktop.
Save haslinger/8956933 to your computer and use it in GitHub Desktop.
Hobo Ajax features and Faye Messaging Server

A few weeks ago I posted about a poor man's chat server I created with Hobo's Ajax features. I now had the request to improve performance, so I tried to fix it.

I pimped my old solution with a Faye messaging server: Ryan Bates created a gem and a Railscast about it.

Lets see how we update data on all connected clients but only when necessary. In conversation.dryml we subscribe to a faye server:

<%= subscribe_to "/conversations/" + this.id.to_s %>

and to a channel for this chat session (called conversation in this app):

<script>
    PrivatePub.subscribe("/conversations/<%= this.id.to_s -%>", function(data, channel) {
      $("form#" + data.type + "-form").submit();
    });
</script>

When the channel fires a callback is triggered, and we immediately select and submit a small form. That is selected by the type that was posted from the create action in the controller:

PrivatePub.publish_to("/conversations/"+ this.conversation.id.to_s, type: "messages")

which itself was called by the form in the view of the person who created the message:

<form with="&@message" update="mymessages">
  <div> New Message: <input:content/> </div>
</form>

Back to the update: The form

<form id="messages-form" style="display:none;" action="refresh" update="messages">
  <input name="id" value="#{this.id}" type="hidden"/>
</form>

calls a minimalist controller action to get the new data:

def refresh
  self.this = Conversation.find(params[:id])
  hobo_show
end

and it updates the part:

  <collection:messages part="messages"/>

So not the data to be displayed is propagated. Just the info, what part has to be updated. Then an ajax call gets the data of the part to be updated. This part then can contain data of any type, e.g. images. See conversation_2.dryml for in example with clipboard upload. These two roundtrips are very small and super fast (< 1K transfered data, 25ms for the ajax call on the server, < 100ms roundtrip. I don't now how to measure the faye roundtrip time.) It looks like real time.

The Faye server comes as a dependency with private_pub and Ryan Bates explains nicely how to set it up in development. I stumbled setting it up on my Ubuntu server with rvm (and passenger). Passenger is the wrong way to go, it is optimized for short requests, Faye sessions are open for a long time (the full conversation).

The easiest way is to create an upstart script (faye.conf). This way you it starts automatically on boot and you can start it manually using

sudo service faye start

You cannot stop, status or restart it using service. Kill is your friend for stopping if necessary, and

netstat -an | grep 9292

for checking status.

Upstart scripts invoking rackup / rvm apps need a wrapper to use the correct ruby and rvm version. That can be created like this:

rvm alias create faye ruby-2.0.0-p353
rvm wrapper faye --no-links rackup

so that the upstart script can use the correct user:

exec sudo -u put_username_in_here /home/put_username_in_here/.rvm/wrappers/faye/rackup /var/rails/mercator/private_pub.ru -s thin -E production

Phew, I guess that's it.

<extend tag="show-page" for="Conversation">
<old-show-page merge>
<append-content-body:>
<form with="&@message" update="mymessages">
<div> New Message: <input:content/> </div>
</form>
<%= subscribe_to "/conversations/" + this.id.to_s %>
<form id="messages-form" style="display:none;" action="refresh" update="messages">
<input name="id" value="#{this.id}" type="hidden"/>
</form>
<form id="downloads-form" style="display:none;" action="refresh" update="downloads">
<input name="id" value="#{this.id}" type="hidden"/>
</form>
<script>
PrivatePub.subscribe("/conversations/<%= this.id.to_s -%>", function(data, channel) {
$("form#" + data.type + "-form").submit();
});
</script>
<section param="collection-section" merge>
<h3 param="collection-heading"> <%= t("attributes.messages") %> </h3>
<collection:messages part="messages"/>
</section>
<section param="collection-section" merge>
<h3 param="collection-heading"> <%= t("attributes.downloads") %> </h3>
<collection:downloads part="downloads"/>
</section>
</append-content-body:>
</old-show-page>
</extend>
<extend tag="show-page" for="Conversation">
<old-show-page merge>
<append-content-body:>
<div class="row">
<div class="span8">
<form with="&@message" update="mymessages">
<div> Neue Nachricht: <input:content/> </div>
</form>
<%= subscribe_to "/conversations/" + this.id.to_s %>
<form id="messages-form" style="display:none;" action="refresh" update="messages">
<input name="id" value="#{this.id}" type="hidden"/>
</form>
<form id="downloads-form" style="display:none;" action="refresh" update="downloads">
<input name="id" value="#{this.id}" type="hidden"/>
</form>
<script>
PrivatePub.subscribe("/conversations/<%= this.id.to_s -%>", function(data, channel) {
$("form#" + data.type + "-form").submit();
});
</script>
<section param="collection-section" merge>
<h3 param="collection-heading"> <%= t("attributes.messages") %> </h3>
<collection:messages part="messages"/>
</section>
<section param="collection-section" merge>
<h3 param="collection-heading"> <%= t("attributes.baskets") %> </h3>
<collection:baskets param/>
</section>
<section param="collection-section" merge>
<h3 param="collection-heading"> <%= t("attributes.downloads") %> </h3>
<collection:downloads part="downloads"/>
</section>
</div>
<div class="span4">
<div id="fine-uploader"></div>
<br/>
<div class="btn btn-info" id="dropzone"> Bild hier pasten (auf mich zeigen und [STRG]+[v])</div>
<script>
var filenames = [];
$(document).ready(function () {
$("#fine-uploader").fineUploader({
multiple: false,
request: {
endpoint: '/sales/conversations/<%= this.id -%>/do_upload',
paramsInBody: true,
customHeaders: {
"X-CSRF-Token": "<%= form_authenticity_token %>"
}
},
paste: {
targetElement: $('#dropzone')[0],
},
params: {
authenticity_token: "<%= form_authenticity_token %>",
}
});
});
// Fineuploader Docs at: http://docs.fineuploader.com/branch/master/api/options.html
// Got the CSRF knowledge from: https://github.com/Widen/fine-uploader/pull/104
</script>
<script type="text/template" id="qq-template">
<div class="qq-uploader-selector qq-uploader">
<div class="qq-upload-button-selector btn btn-success">
<div>Datei hochladen</div>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Datei wird verarbeitet...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list">
<li>
<div class="qq-progress-bar-container-selector">
<div class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon"></span>
<span class="qq-upload-file-selector qq-upload-file"></span>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text"/>
<span class="qq-upload-size-selector qq-upload-size"></span>
<a class="qq-upload-cancel-selector qq-upload-cancel" href="#">Abbrechen</a>
<a class="qq-upload-retry-selector qq-upload-retry" href="#">Wiederholen</a>
<a class="qq-upload-delete-selector qq-upload-delete" href="#">Löschen</a>
<span class="qq-upload-status-text-selector qq-upload-status-text"></span>
</li>
</ul>
</div>
</script>
</div>
</div>
</append-content-body:>
</old-show-page>
</extend>
# I live in app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController
hobo_model_controller
auto_actions :show, :index, :lifecycle
def refresh
self.this = Conversation.find(params[:id])
hobo_show
end
# ...
end
# I live in /etc/init.d/faye.conf
description "faye"
version "1.0"
author "Stefan Haslinger"
env LANG=de_AT.UTF-8
env APP_ROOT=/var/rails/mercator
start on startup
stop on shutdown
respawn
script
cd $APP_ROOT
exec sudo -u put_username_in_here /home/put_username_in_here/.rvm/wrappers/faye/rackup /var/rails/mercator/private_pub.ru -s thin -E production
end script
# ...
gem 'private_pub' # Private Pub Sub System using Faye
# ...
# I live in app/controllers/messages_controller.rb
class MessagesController < ApplicationController
hobo_model_controller
auto_actions :create, :show, :index
def create
hobo_create do
this.sender = current_user
this.save
PrivatePub.publish_to("/conversations/"+ this.conversation.id.to_s, type: "messages")
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment