Skip to content

Instantly share code, notes, and snippets.

@Epigene
Last active February 10, 2016 13:43
Show Gist options
  • Save Epigene/8b5a2e9693ab17e8b3d1 to your computer and use it in GitHub Desktop.
Save Epigene/8b5a2e9693ab17e8b3d1 to your computer and use it in GitHub Desktop.
Integrating Sync realtime partials in a rails app | Estimated difficulty: Medium, time required: 65 minutes

Setup (ABandoned, awaiting Rails5 ActionCable)

1 Setup gems

We will be using puma instead of thin.

gem 'faye'
gem 'puma'
gem 'sync', '~> 0.3.0'
2 Install
$ bundle
$ rails g sync:install
3 Require sync in JS/Coffee manifest
#= require sync
4 Require Pub/Sub adapter JS in application.slim
= javascript_include_tag Sync.adapter_javascript_url
5 Configure faye
# in config/sync.yml
development:
  server: "http://localhost:9292/faye"
  adapter_javascript_url: "http://localhost:9292/faye/faye.js"
  auth_token: "badass"
  adapter: "Faye"
  async: true

test:
  server: "http://localhost:9292/faye"
  adapter_javascript_url: "http://localhost:9292/faye/faye.js"
  adapter: "Faye"
  auth_token: "secret"
  async: false

production:
  server: "http://example.com/faye"
  adapter_javascript_url: "http://localhost:9292/faye/faye.js"
  adapter: "Faye"
  auth_token: "4f13884bb7e40b5573c2962c520d7b6a57a40f319fb686cb219d606e756ce118"
  async: true
6 Configure sync.ru
# Run with: rackup sync.ru -E production
require "bundler/setup"
require "yaml"
require "faye"
require "sync"

Faye::WebSocket.load_adapter 'puma'

Sync.load_config(
  File.expand_path("../config/sync.yml", __FILE__),
  ENV["RAILS_ENV"] || "development"
)

run Sync.pubsub_app
7 Make Sync run with app boot
# in /config/initializers/sync.rb
Thread.new do
  env = ENV["RAILS_ENV"] || "production"
  # `ps -ef | grep "sync" | awk '{print $2}' | xargs kill`
  puts "Booting Faye backend for Sync in #{env} environment..."
  system("rackup sync.ru -E production")
end
8 Last touches
$ rake assets:clobber

Make structure

Sync is very particular about naming conventions, it is what allows it to do the magic.
Suppose you have a Comment model with a single :body attribute and want an index view where a stream of latest comments would drop in. Assuming Rails-way, Comment model stores information in database table named comments. You need a partial for comment rows in

# in /app/views/sync/<model_table_name>/_<model_name>_row.slim
/app/views/sync/comments/_comment_row.slim

The partial file name can be arbitrary, but it helps to keep with the naming convention. And, of course, routes: resources :comments, only: :index

1 Adapt Comments controller

Key is to add the enable_sync part. This will subscribe opening browsers to realtime messages.

class CommentsController < ApplicationController
  enable_sync only: [:index, :create]
  
  def index
  end
  
  def create
    [...]
    sync_new @comment, partial: 'comment_row'
  end
end
2 Make view

Here sync_new will catch new model creations and sync will render existing model instances and listen for updates or destroys.

# in /views/comments/index.slim

h1 Comment stream
= sync_new partial: 'comment_row', resource: Comment.new, direction: :prepend
/ or :append to put at the bottom
= sync partial: 'comment_row', collection: Comment.all_comments

If your objects will never be edited, only added, you can load existing comments without the sync listening wrapper in a regular partial:

h1 Comment stream
= sync_new partial: 'comment_row', resource: Comment.new
/= render partial: 'comment_row', collection: Comment.all_comments
- Comment.all_comments.each do |comment|
  = render partial: 'sync/comments/comment_row', locals: {comment: comment}
3 Make sync partial

No problem here, you only need to keep realtime reloadable parts under /views/sync/<model_table_name>
Here we will simply display the comment's text body

/in /views/sync/comments/_comment_row.slim
p
  = comment.body

Tweak Model code

1 Adapt Comment model
class Comment < ActiveRecord::Base
  include Sync::Actions # Enables syncing from console, jobs, callbacks etc.
  sync :all
  sync_scope :all_comments, -> { all.order(created_at: :desc) } # This is how sync-used scopes are defined
  
  ### Experimental create & update method overrides
  def update *args
    Sync::Model.enable { super }
  end

  def self.create(attributes = {}, &block) #(attributes = nil, options = {}, &block)
    Sync::Model.enable { super }
  end

  def self.create!(attributes = {}, &block) #(attributes = nil, options = {}, &block)
    Sync::Model.enable { super }
  end

  def save *args
    Sync::Model.enable { super }
  end

  def save! *args
    Sync::Model.enable { super }
  end

Testing

With everything set up like this:

  1. run faye with $ rackup sync.ru -E production # NB, must run in production environment even in dev, otherwise lint errors
  2. run rails server
  3. run rails c
  4. navigate to /comments and see nothing (since no comments yet exist)
  5. in console run Comment.create!(body: "New comment at #{Time.now}")
  6. in console you should see "Rendered sync/comments/_comment_row.slim" somewhere after COMMIT
  7. in /comments you should see the new comment body text appeared.
  8. Horray, it is realtime!

Deploying to production

Assuming your production environment uses SSL, faye server should as well.

Troubleshooting and gotchas

Place javascript asset requirements from step 3 & 4 in correct namespace - if developing functionality for admin, require sync in admin.coffee and admin.slim respectively.
Sync partials are finnicky in respect to nesting, best make them individual paragraphs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment