├── .gitignore ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── app └── views │ └── realtime │ ├── _realtime_loader.html.erb │ ├── _realtime_message_console_logger.html.erb │ ├── _realtime_message_handler.html.erb │ ├── async_realtime_support.html.erb │ ├── realtime_message_console_logger.html.erb │ ├── realtime_message_handler.html.erb │ └── realtime_support.html.erb ├── lib ├── realtime.rb └── realtime │ ├── railtie.rb │ ├── realtime_controller.rb │ ├── redis_wrapper.rb │ ├── version.rb │ ├── view_helpers.rb │ └── zmq_wrapper.rb └── realtime.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | realtime (0.0.1) 5 | rails (~> 4.1.1) 6 | redis 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (4.1.2) 12 | actionpack (= 4.1.2) 13 | actionview (= 4.1.2) 14 | mail (~> 2.5.4) 15 | actionpack (4.1.2) 16 | actionview (= 4.1.2) 17 | activesupport (= 4.1.2) 18 | rack (~> 1.5.2) 19 | rack-test (~> 0.6.2) 20 | actionview (4.1.2) 21 | activesupport (= 4.1.2) 22 | builder (~> 3.1) 23 | erubis (~> 2.7.0) 24 | activemodel (4.1.2) 25 | activesupport (= 4.1.2) 26 | builder (~> 3.1) 27 | activerecord (4.1.2) 28 | activemodel (= 4.1.2) 29 | activesupport (= 4.1.2) 30 | arel (~> 5.0.0) 31 | activesupport (4.1.2) 32 | i18n (~> 0.6, >= 0.6.9) 33 | json (~> 1.7, >= 1.7.7) 34 | minitest (~> 5.1) 35 | thread_safe (~> 0.1) 36 | tzinfo (~> 1.1) 37 | arel (5.0.1.20140414130214) 38 | builder (3.2.2) 39 | erubis (2.7.0) 40 | hike (1.2.3) 41 | i18n (0.6.9) 42 | json (1.8.1) 43 | mail (2.5.4) 44 | mime-types (~> 1.16) 45 | treetop (~> 1.4.8) 46 | mime-types (1.25.1) 47 | minitest (5.3.5) 48 | multi_json (1.10.1) 49 | polyglot (0.3.5) 50 | rack (1.5.2) 51 | rack-test (0.6.2) 52 | rack (>= 1.0) 53 | rails (4.1.2) 54 | actionmailer (= 4.1.2) 55 | actionpack (= 4.1.2) 56 | actionview (= 4.1.2) 57 | activemodel (= 4.1.2) 58 | activerecord (= 4.1.2) 59 | activesupport (= 4.1.2) 60 | bundler (>= 1.3.0, < 2.0) 61 | railties (= 4.1.2) 62 | sprockets-rails (~> 2.0) 63 | railties (4.1.2) 64 | actionpack (= 4.1.2) 65 | activesupport (= 4.1.2) 66 | rake (>= 0.8.7) 67 | thor (>= 0.18.1, < 2.0) 68 | rake (10.3.2) 69 | redis (3.1.0) 70 | sprockets (2.12.1) 71 | hike (~> 1.2) 72 | multi_json (~> 1.0) 73 | rack (~> 1.0) 74 | tilt (~> 1.1, != 1.3.0) 75 | sprockets-rails (2.1.3) 76 | actionpack (>= 3.0) 77 | activesupport (>= 3.0) 78 | sprockets (~> 2.8) 79 | thor (0.19.1) 80 | thread_safe (0.3.4) 81 | tilt (1.4.1) 82 | treetop (1.4.15) 83 | polyglot 84 | polyglot (>= 0.3.1) 85 | tzinfo (1.2.1) 86 | thread_safe (~> 0.1) 87 | 88 | PLATFORMS 89 | ruby 90 | 91 | DEPENDENCIES 92 | realtime! 93 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Mike Atlas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Realtime Support for Rails 2 | ==== 3 | 4 | Note: This gem is a concept piece originally conceived in 2013 and published as a generic open source project in early 2014. 5 | 6 | As of mid-2015, support for performant, native and scalable websockets are available in Rails. See ~~[ActiveCable](https://github.com/rails/actioncable)~~ [ActionCable](https://github.com/rails/rails/tree/master/actioncable), which landed in Rails 5 and will probably be officially released early/mid 2016. 7 | 8 | As such, with [`ActionCable`'s design](http://weblog.rubyonrails.org/2016/2/2/Rails-5-0-beta2/), you don't even need a separate pub/sub server (redis) and Node.js running anymore to achieve similar lightweight realtime bi-directional communication with a large number of connected clients to your Rails application. 9 | 10 | Progress marches forward! I presume, if you are stuck and can't upgrade to Rails 5 anytime soon, perhaps this project is lightweight compared to some of the heavier alternatives like Faye or PubNub. 11 | 12 | Full documentation is available at: 13 | 14 | http://mikeatlas.github.io/realtime-rails/ 15 | -------------------------------------------------------------------------------- /app/views/realtime/_realtime_loader.html.erb: -------------------------------------------------------------------------------- 1 | if (window.realtime.socketIo) { 2 | 3 | window.realtime.enabled = true; 4 | 5 | window.realtime.socketIo.on('connect', function() { 6 | // Give a nice round-trip ACK to our realtime server that we connected. 7 | window.realtime.socketIo.emit('realtime_user_id_connected', { userId: window.realtime.userId }); 8 | }); 9 | 10 | // Queue up all incoming realtime messages. 11 | window.realtime.socketIo.on('realtime_msg', function(message) { 12 | window.realtime.messageQueue.push(message); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /app/views/realtime/_realtime_message_console_logger.html.erb: -------------------------------------------------------------------------------- 1 | if (window.realtime.enabled && window.realtime.eventBus){ 2 | // handle events in the queue with eventing 3 | var realtimeMessageEventConsoleLogger = function(message) { 4 | console.log(message); 5 | }; 6 | window.realtime.eventBus.on('realtimeMessage', realtimeMessageEventConsoleLogger); 7 | 8 | } else if (window.realtime.enabled) { 9 | // handle events in the queue without eventing 10 | messageQueueConsoleLogger = function() { 11 | message = window.realtime.messageQueue.shift(); 12 | if (message) { 13 | console.log(message); 14 | } 15 | }; 16 | setInterval(messageQueueConsoleLogger, 100); 17 | 18 | } else { 19 | console.log('Error: Realtime was not enabled.') 20 | } 21 | -------------------------------------------------------------------------------- /app/views/realtime/_realtime_message_handler.html.erb: -------------------------------------------------------------------------------- 1 | if (window.realtime.enabled){ 2 | 3 | window.realtime.eventBus = _.extend({}, Backbone.Events); 4 | 5 | messageQueueRunner = function() { 6 | message = window.realtime.messageQueue.shift(); 7 | if (message) { 8 | window.realtime.eventBus.trigger('realtimeMessage', message); 9 | } 10 | }; 11 | 12 | setInterval(messageQueueRunner, 100); 13 | } 14 | -------------------------------------------------------------------------------- /app/views/realtime/async_realtime_support.html.erb: -------------------------------------------------------------------------------- 1 | 72 | -------------------------------------------------------------------------------- /app/views/realtime/realtime_message_console_logger.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/views/realtime/realtime_message_handler.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/views/realtime/realtime_support.html.erb: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 22 | 23 | <% if message_handler %> 24 | 25 | 26 | 27 | 28 | 31 | <% end %> 32 | 33 | <% if message_console_logger %> 34 | 37 | <% end %> 38 | -------------------------------------------------------------------------------- /lib/realtime.rb: -------------------------------------------------------------------------------- 1 | 2 | require "realtime/realtime_controller.rb" 3 | require "realtime/redis_wrapper.rb" 4 | require "realtime/zmq_wrapper.rb" 5 | require 'realtime/view_helpers.rb' 6 | require 'realtime/railtie.rb' 7 | -------------------------------------------------------------------------------- /lib/realtime/railtie.rb: -------------------------------------------------------------------------------- 1 | module Realtime 2 | 3 | class Railtie < Rails::Railtie 4 | 5 | initializer "realtime.action_controller" do 6 | ActiveSupport.on_load(:action_controller) do 7 | ActionController::Base.send :include, Realtime::Controller 8 | end 9 | end 10 | 11 | initializer "realtime.view_helpers" do 12 | ActionView::Base.send :include, ViewHelpers 13 | 14 | # kinda blah way to force rails to load our helper views path 15 | curr_path = File.expand_path(File.dirname(__FILE__)) 16 | view_path = "#{curr_path}/../../app/views" 17 | ActionController::Base.append_view_path(view_path) 18 | end 19 | 20 | end 21 | 22 | end -------------------------------------------------------------------------------- /lib/realtime/realtime_controller.rb: -------------------------------------------------------------------------------- 1 | module Realtime 2 | module Controller 3 | extend ActiveSupport::Concern 4 | 5 | module ClassMethods 6 | def realtime_controller(options = {}) 7 | queue = options.delete(:queue) 8 | before_action :do_realtime_token, options 9 | before_action :do_realtime_user_id, options 10 | before_action :do_realtime_server_url, options 11 | if queue.nil? || queue == :redis 12 | after_action :store_realtime_session_redis, options 13 | elsif queue == :zmq 14 | after_action :store_realtime_session_zmq, options 15 | end 16 | end 17 | end 18 | 19 | def do_realtime_token 20 | @realtime_token = Digest::MD5.hexdigest("#{session[:session_id]}:#{realtime_user_id}") 21 | return @realtime_token 22 | end 23 | 24 | def do_realtime_user_id 25 | @realtime_user_id = realtime_user_id 26 | return @realtime_user_id 27 | end 28 | 29 | def do_realtime_server_url 30 | @realtime_server_url = realtime_server_url 31 | return @realtime_server_url 32 | end 33 | 34 | # create shared session tokens for redis/socketio realtime server running on node.js 35 | def store_realtime_session_zmq 36 | # store session data or any authentication data you want here, generate to JSON data 37 | session_data = { 38 | "user_id" => realtime_user_id, 39 | } 40 | 41 | # todo: merge additional session data passed in 42 | stored_session_data = JSON.generate(session_data) 43 | 44 | ZmqWrapper.store_session( 45 | realtime_user_id, 46 | @realtime_token, 47 | stored_session_data, 48 | 86400 49 | ) 50 | end 51 | 52 | # create shared session tokens for redis/socketio realtime server running on node.js 53 | def store_realtime_session_redis 54 | # store session data or any authentication data you want here, generate to JSON data 55 | session_data = { 56 | "user_id" => realtime_user_id, 57 | } 58 | 59 | # todo: merge additional session data passed in 60 | 61 | stored_session_data = JSON.generate(session_data) 62 | 63 | RedisWrapper.redis.hset( 64 | "rtSession-" + realtime_user_id.to_s, 65 | @realtime_token, 66 | stored_session_data, 67 | ) 68 | 69 | # expire this realtime session after one day. 70 | RedisWrapper.redis.expire("rtSession-" + realtime_user_id.to_s, 86400) 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/realtime/redis_wrapper.rb: -------------------------------------------------------------------------------- 1 | class RedisWrapper 2 | 3 | def self.publish(message) 4 | redis.publish 'realtime_msg', message.to_json 5 | end 6 | 7 | def redis 8 | return $redis # global set in redis.rb initializer 9 | end 10 | 11 | def self.redis 12 | return $redis # global set in redis.rb initializer 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /lib/realtime/version.rb: -------------------------------------------------------------------------------- 1 | module Realtime 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/realtime/view_helpers.rb: -------------------------------------------------------------------------------- 1 | module Realtime 2 | module ViewHelpers 3 | 4 | def realtime_support(args = {}) 5 | async = args[:async] ? 'async_' : '' 6 | return render(template: "realtime/#{async}realtime_support", 7 | layout: nil, 8 | locals: 9 | { realtime_token: @realtime_token, 10 | realtime_domain: @realtime_domain, 11 | realtime_server_url: @realtime_server_url, 12 | realtime_user_id: @realtime_user_id, 13 | message_handler: args[:message_handler], 14 | message_console_logger: args[:message_console_logger] }).to_s 15 | end 16 | 17 | def realtime_message_handler 18 | ActiveSupport::Deprecation.warn("'realtime_message_handler' is deprecated, "\ 19 | "please refer to the documentation for details.") 20 | return render(template: "realtime/realtime_message_handler", 21 | layout: nil, 22 | locals: {}).to_s 23 | end 24 | 25 | def realtime_message_console_logger 26 | ActiveSupport::Deprecation.warn("'realtime_message_console_logger' is deprecated, "\ 27 | "please refer to the documentation for details.") 28 | return render(template: "realtime/realtime_message_console_logger", 29 | layout: nil, 30 | locals: {}).to_s 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/realtime/zmq_wrapper.rb: -------------------------------------------------------------------------------- 1 | class ZmqWrapper 2 | 3 | if defined?(ZMQ) && defined?(EmZeromq) 4 | require 'em-zeromq' 5 | 6 | def self.publish(the_message) 7 | EM.run { 8 | zmq = EM::ZeroMQ::Context.new(1) 9 | pusher = zmq.socket(ZMQ::PUSH) 10 | pusher.connect($zmq_server) 11 | message = "realtime_msg:" + the_message.to_json 12 | puts "Pushing realtime message: " + message 13 | pusher.send_msg(message) 14 | EM.stop 15 | } 16 | end 17 | 18 | def self.store_session(user_id, session_id, session_data, expiration) 19 | EM.run { 20 | zmq = EM::ZeroMQ::Context.new(1) 21 | pusher = zmq.socket(ZMQ::PUSH) 22 | pusher.connect($zmq_server) 23 | the_message = {user_id: user_id, 24 | session_id: session_id, 25 | session_data: session_data, 26 | expiration: expiration} 27 | message = "rtSession:" + the_message.to_json 28 | puts "Pushing session message: " + message 29 | pusher.send_msg(message) 30 | EM.stop 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /realtime.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "realtime/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "realtime" 9 | s.version = Realtime::VERSION 10 | s.authors = ["Mike Atlas", "Ahmad Abdel-Yaman (@ayaman)", "Nick Prokesch (@prokizzle)"] 11 | s.email = ["mike.atlas@gmail.com"] 12 | s.homepage = "http://mikeatlas.github.io/realtime-rails/" 13 | s.summary = "Realtime support for Rails applications." 14 | s.description = "Provides a simple Realtime framework for Rails applications." 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_dependency "rails", ">= 4.0" 21 | 22 | #s.add_dependency "redis" 23 | #s.add_dependency "zmq" 24 | #s.add_dependency "em-zmq" 25 | 26 | end 27 | --------------------------------------------------------------------------------