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 +3 -0
- data/Gemfile +3 -0
- data/README.md +35 -0
- data/Rakefile +4 -0
- data/lib/real_time_rails.rb +13 -0
- data/lib/real_time_rails/ar.rb +25 -0
- data/lib/real_time_rails/real_time_helper.rb +12 -0
- data/lib/real_time_rails/render_real_time_controller.rb +30 -0
- data/lib/real_time_rails/rt_helper.rb +127 -0
- data/lib/real_time_rails/version.rb +5 -0
- data/lib/websocket_server/websocket_server.rb +143 -0
- data/real_time_rails.gemspec +28 -0
- metadata +89 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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,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,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
|
+
|