real_time_rails 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gemspec
2
+ pkg
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ About
2
+ =====
3
+
4
+ RealTimeRails gem to enable seamless websocket integration with rails.
5
+
6
+
7
+ Purpose
8
+ =======
9
+
10
+ Implement a gem that will add a new render method that sets up a connection to a websocket server and notifies the server it's waiting for updates to content related to the specific partial.
11
+
12
+ During an update to an active record object, the websocket server gets a notice from the server to send updates to the connected clients for the content they are listening for.
13
+
14
+
15
+ Disclaimer
16
+ ----------
17
+
18
+ All source code at this point is to portray ideas to further cooperative design. It is not ready for use nor tested for validity.
19
+
20
+ Beta Usage
21
+ ----------
22
+
23
+ The gem is now loading and running correctly in the project. Still some bugs to iron out.
24
+
25
+ To start the websocket server just run the websocket_server.rb ruby script.
26
+
27
+ in your models that you want real time updates
28
+
29
+ `include RealTimeRails:AR`
30
+
31
+ then in your view that you want a real time update. At this point partial paths must be full view paths.
32
+
33
+ `render_real_time partial: '/test/test', locals: {chats: @chats}`
34
+
35
+ I still have a lot of debugging stuff in the view and javascript wrapper so ignore those for now.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'cucumber/rake/task'
3
+
4
+ Cucumber::Rake::Task.new
@@ -0,0 +1,13 @@
1
+ require 'real_time_rails/version.rb'
2
+ require 'real_time_rails/real_time_helper.rb'
3
+ require 'real_time_rails/render_real_time_controller.rb'
4
+ require 'real_time_rails/ar.rb'
5
+ require 'real_time_rails/rt_helper.rb'
6
+
7
+ if defined?(ActionView::Base)
8
+ ActionView::Base.send :include, RealTimeRails::RealTimeHelper
9
+ end
10
+
11
+
12
+
13
+
@@ -0,0 +1,25 @@
1
+ module RealTimeRails
2
+
3
+ # include RealTimeRails:AR in your model for access to realtime updates.
4
+ module AR
5
+ #after every save send notification to the realtimerails socket server.
6
+ def self.included(klass)
7
+ klass.send :after_save, :send_rtr_update
8
+ end
9
+
10
+ private
11
+
12
+ def send_rtr_update
13
+ # TODO figure out why i have to make 2 connections to send 2 messages instead of just one connection.
14
+
15
+ mySock = TCPSocket::new('127.0.0.1', 2000)
16
+ mySock.puts("{\"command\":\"update1\",\"model\":\"#{self.class.name}\",\"id\":\"#{self.id}\"}")
17
+ mySock.close
18
+
19
+ mySock = TCPSocket::new('127.0.0.1', 2000)
20
+ mySock.puts("{\"command\":\"updateall\",\"model\":\"#{self.class.name}\"}")
21
+ mySock.close
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,12 @@
1
+ module RealTimeRails
2
+ module RealTimeHelper
3
+
4
+ def render_real_time(options = {})
5
+ RealTimeRails::RtrHelper.new(options) do
6
+ render options
7
+ end.html
8
+ end
9
+
10
+ end
11
+ end
12
+
@@ -0,0 +1,30 @@
1
+ class RenderRealTimeController < ActionController::Base
2
+
3
+ # Updates will pull data from this controller.
4
+ # url in the form of '/render_real_time/id/#{md5_hash_id}'
5
+ # The information is pulled from Rails.cache by the md5 hash id
6
+
7
+ def id
8
+ websocket_options = YAML.load(Rails.cache.read("real_time_#{params[:id]}"))
9
+ options = YAML.load(Rails.cache.read("real_time_#{params[:id]}_options"))
10
+ locals = {}
11
+ websocket_options[:models].each do |rtmodel|
12
+
13
+ case rtmodel[:type]
14
+ when :single
15
+ locals[rtmodel[:key]] = eval("#{rtmodel[:name]}.find(rtmodel[:id])")
16
+ when :array
17
+ locals[rtmodel[:key]] = eval("#{rtmodel[:name]}.find(rtmodel[:ids])")
18
+ when :relation
19
+ rtmodel[:sql] = rtmodel[:sql].gsub('\"','"')
20
+ locals[rtmodel[:key]] = eval("#{rtmodel[:name]}.find_by_sql(rtmodel[:sql])") #TODO This needs to recreate the arel object. Not just find_by_sql.
21
+ else
22
+
23
+ end
24
+ end
25
+ render :update do |page|
26
+ page.replace_html params[:id], :partial => options[:partial], :locals => locals
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,127 @@
1
+ module RealTimeRails
2
+ class RtrHelper
3
+
4
+ require 'digest/md5'
5
+
6
+ include ActionView::Helpers::UrlHelper
7
+ include ActionView::Helpers::PrototypeHelper
8
+ include ActionView::Helpers::JavaScriptHelper
9
+
10
+ def protect_against_forgery?
11
+ false
12
+ end
13
+
14
+ attr_accessor :options,
15
+ :id,
16
+ :html,
17
+ :websocket_options,
18
+ :javascript_options,
19
+ :render_options,
20
+ :wrap_options,
21
+ :remote_f_options
22
+
23
+
24
+ def initialize(options = {})
25
+ @options = options
26
+ set_options
27
+ register_partial
28
+ html = load_javascript
29
+ html += manual_buttons #TODO remove test helper for ajax update calls.
30
+ html += wrap_render do
31
+ yield
32
+ end
33
+ @html = html
34
+ end
35
+
36
+ def set_options
37
+ model_list = []
38
+ @options[:locals].each do |key,value|
39
+ if value.is_a?(ActiveRecord::Base)
40
+ model_list << {type: :single, key: key, name: value.class.name, id: value.id}
41
+ end
42
+ if value.is_a?(Array)
43
+ if (class_name = value.map{|v| v.class.name}.uniq).length==1
44
+ model_list << {type: :array, key: key, name: class_name.first, ids: value.map(&:id)}
45
+ else
46
+ raise "Can not do real time updates on arrays containing different models.\n#{value.map{|v| v.class.name}.uniq.to_yaml}"
47
+ end
48
+ end
49
+ if value.is_a?(ActiveRecord::Relation)
50
+ model_list << {type: :relation, key: key, name: value.ancestors.first.name, sql: value.to_sql.gsub('"','\"')}
51
+ end
52
+ end
53
+ @websocket_options = {
54
+ models: model_list,
55
+ command: 'listen'
56
+ }
57
+ @id = Digest::MD5.hexdigest(@websocket_options.to_yaml)
58
+ @websocket_options = {
59
+ models: model_list,
60
+ command: 'listen',
61
+ id: @id
62
+ }
63
+ end
64
+
65
+ # TODO remove test helper method for ajax update calls.
66
+ def manual_buttons
67
+ "<a href='#' onclick='real_time_update_#{@id}();'>Manual Update</a>\n"
68
+ end
69
+
70
+ def load_javascript
71
+ @remote_f_options = {
72
+ url: "/render_real_time/id/#{@id}"
73
+ }
74
+
75
+ html = "<script>\n"
76
+ html += js_after_update
77
+ html += js_remote_function
78
+ html += js_start_websocket
79
+ html += "</script>\n"
80
+ return html
81
+ end
82
+
83
+
84
+ # Adds the wrapper for creating the connection to the websocket server as well as registering for the correct channel on the server.
85
+ def js_start_websocket
86
+ "
87
+ ws_#{@id} = new WebSocket('ws://localhost:8080');
88
+ ws_#{@id}.onmessage = function(evt) { if(evt.data=='update'){real_time_update_#{@id}()}else{alert(evt.data)}; };
89
+ ws_#{@id}.onclose = function() { };
90
+ ws_#{@id}.onopen = function() {
91
+ ws_#{@id}.send('#{@websocket_options.to_json}');
92
+ };
93
+ "
94
+ end
95
+
96
+ def js_after_update
97
+ if @options[:after_update]
98
+ @remote_f_options[:complete] = "after_real_time_update_#{@id}();"
99
+ return "function after_real_time_update_#{@id}(){#{@options[:after_update]}}"
100
+ else
101
+ return ""
102
+ end
103
+ end
104
+
105
+ # Creates the js method wrapper for ajax calls.
106
+ def js_remote_function
107
+ "function real_time_update_#{@id}(){
108
+ #{remote_function(remote_f_options)}
109
+ }"
110
+ end
111
+
112
+ def wrap_render
113
+ html = "<div id='#{@id}' class='real_time_wrapper'>\n"
114
+ html += yield
115
+ html += @websocket_options.to_yaml # TODO remove debugging data.
116
+ html += "</div>\n"
117
+ return html
118
+ end
119
+
120
+
121
+ # Writes data to cache for later use in the render_real_time controller.
122
+ def register_partial
123
+ Rails.cache.write("real_time_#{@id}", @websocket_options.to_yaml)
124
+ Rails.cache.write("real_time_#{@id}_options", @options.to_yaml)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,5 @@
1
+ module RealTimeRails
2
+
3
+ VERSION = "0.0.2"
4
+
5
+ end
@@ -0,0 +1,143 @@
1
+ # to start the server just run "ruby websocket_server.rb"
2
+
3
+ require 'em-websocket'
4
+ require 'json'
5
+ require 'yaml'
6
+
7
+ class RTChannel < EM::Channel
8
+
9
+ attr_accessor :id,
10
+ :models,
11
+ :subscribers
12
+
13
+ def self.create(id, models)
14
+ sid = self.new
15
+ sid.id=id
16
+ sid.models=models
17
+ sid.subscribers=[]
18
+ return sid
19
+ end
20
+
21
+ def join(subscriber)
22
+ @subscribers << subscriber
23
+ end
24
+
25
+ def leave(subscriber, subscription_id)
26
+ @subscribers.delete(subscriber)
27
+ self.unsubscribe(subscription_id)
28
+ end
29
+
30
+ end
31
+
32
+ EventMachine.run {
33
+
34
+
35
+ #$global_channel = EM::Channel.new
36
+ $channel_list = []
37
+
38
+
39
+ EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080) do |ws|
40
+ ws.onopen do
41
+ puts "opened conneciton"
42
+ end
43
+ ws.onmessage do |msg|
44
+ data = JSON.parse(msg)
45
+ puts "new message:\n#{msg}"
46
+ @subscriber = data["subscriber"]
47
+ case data["command"]
48
+ when "listen"
49
+ unless @channel = $channel_list.find{|channel| channel.id == data["id"]}
50
+ @channel = RTChannel.create(data["id"], data["models"])
51
+ $channel_list << @channel
52
+ end
53
+ @sid = @channel.subscribe{|msg| ws.send msg}
54
+ @channel.join(@subscriber)
55
+ else
56
+ puts "unknown command: #{msg}"
57
+ end
58
+ end
59
+ ws.onclose {
60
+ # $channel_list.each do |channel|
61
+ # if channel.subscribers.include?(@subscriber)
62
+ @channel.leave(@subscriber, @sid)
63
+ # end
64
+ # end
65
+ }
66
+ end
67
+
68
+ module MessageServer
69
+
70
+ def post_init
71
+ send_data "connected to update server\n"
72
+ puts "new connection\n"
73
+ end
74
+
75
+ def unbind
76
+ #puts "-- someone disconnected from the server!"
77
+ end
78
+
79
+ def find_channels_by_id(model_name, id =nil)
80
+ channel_list = []
81
+ $channel_list.each{|channel|
82
+ if models = channel.models.select{|m| m["name"]==model_name}
83
+ models.each do |m|
84
+ if (m["type"] == "single") && (m["id"].to_s == id.to_s) && (m["name"] == model_name)
85
+ channel_list << channel
86
+ end
87
+ if (m["type"] == "array") && (m["ids"].map(&:to_s).include?(id.to_s)) && (m["name"] == model_name)
88
+ channel_list << channel
89
+ end
90
+ end
91
+
92
+ end
93
+ }
94
+ return channel_list
95
+ end
96
+
97
+ def find_all_channels(model_name)
98
+ $channel_list.select{|channel|
99
+ channel.models.map{|m| m["name"]}.include?(model_name) &&
100
+ channel.models.map{|m| m["type"]}.include?("relation")
101
+ }
102
+ end
103
+
104
+
105
+ def receive_data data
106
+ close_connection if data =~ /quit/i
107
+ if data =~ /info/i
108
+ send_data "channels:\n#{$channel_list.to_yaml}\n"
109
+ else
110
+ begin
111
+ command = JSON.parse(data)
112
+ puts command
113
+ case command["command"]
114
+ when "update1"
115
+ channels = find_channels_by_id(command["model"], command["id"])
116
+ channels.each{|c| c.push "update"}
117
+ when "updateall"
118
+ channels=find_all_channels(command["model"])
119
+ channels.each{|c| c.push "update"}
120
+ when "info"
121
+ send_data "channels:\n#{$channel_list.to_yaml}\n"
122
+ else
123
+ puts "unknown command: #{command["command"]}\n"
124
+ send_data "unknown command: #{command["command"]}\n"
125
+ end
126
+ rescue JSON::ParserError
127
+ puts "unknown command: \n#{data}"
128
+ send_data "unknown command:\n#{data}"
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ EventMachine::start_server "0.0.0.0", 2000, MessageServer
135
+ puts 'Running message server on 2000'
136
+
137
+ # is ping needed to keep a connection open?
138
+ # EventMachine::PeriodicTimer.new(5) do
139
+ # $channel.push "ping"
140
+ # end
141
+
142
+ puts "loaded"
143
+ }
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "real_time_rails"
3
+ s.version = '0.0.2'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ["Kelly Mahan"]
6
+ s.email = 'kmahan@kmahan.com'
7
+ s.summary = 'A gem to enable seamless websocket integration with rails.'
8
+ s.homepage = 'http://github.com/kellymahan/RealTimeRails'
9
+ s.description = 'A gem to enable seamless websocket integration with rails.'
10
+
11
+
12
+ s.rubyforge_project = 'real_time_rails'
13
+
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_path = 'lib'
19
+
20
+
21
+ s.requirements << "em-websocket"
22
+ s.requirements << "json"
23
+
24
+
25
+ s.add_dependency "em-websocket", ">= 0.3.0"
26
+ s.add_dependency "rails", ">= 3.0.5"
27
+
28
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: real_time_rails
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.2
6
+ platform: ruby
7
+ authors:
8
+ - Kelly Mahan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-06-01 00:00:00 -05:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: em-websocket
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 0.3.0
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 3.0.5
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ description: A gem to enable seamless websocket integration with rails.
39
+ email: kmahan@kmahan.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - .gitignore
48
+ - Gemfile
49
+ - README.md
50
+ - Rakefile
51
+ - lib/real_time_rails.rb
52
+ - lib/real_time_rails/ar.rb
53
+ - lib/real_time_rails/real_time_helper.rb
54
+ - lib/real_time_rails/render_real_time_controller.rb
55
+ - lib/real_time_rails/rt_helper.rb
56
+ - lib/real_time_rails/version.rb
57
+ - lib/websocket_server/websocket_server.rb
58
+ - real_time_rails.gemspec
59
+ has_rdoc: true
60
+ homepage: http://github.com/kellymahan/RealTimeRails
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ requirements:
81
+ - em-websocket
82
+ - json
83
+ rubyforge_project: real_time_rails
84
+ rubygems_version: 1.5.2
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: A gem to enable seamless websocket integration with rails.
88
+ test_files: []
89
+