websocket-rails 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,6 +3,7 @@ coverage/
3
3
  .DS_Store
4
4
  .bundle/
5
5
  log/*.log
6
+ *.log
6
7
  pkg/
7
8
  doc/
8
9
  test/dummy/db/*.sqlite3
data/Gemfile CHANGED
@@ -5,4 +5,6 @@ gemspec
5
5
  gem "rspec-rails"
6
6
  gem "eventmachine", ">= 1.0.0.beta.3"
7
7
  gem "faye-websocket"
8
- gem "simplecov"
8
+ gem "simplecov"
9
+ gem "guard"
10
+ gem "guard-rspec"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- websocket-rails (0.1.1)
4
+ websocket-rails (0.1.2)
5
5
  faye-websocket
6
6
  rack
7
7
  thin
@@ -9,101 +9,124 @@ PATH
9
9
  GEM
10
10
  remote: http://rubygems.org/
11
11
  specs:
12
- abstract (1.0.0)
13
- actionmailer (3.0.12)
14
- actionpack (= 3.0.12)
15
- mail (~> 2.2.19)
16
- actionpack (3.0.12)
17
- activemodel (= 3.0.12)
18
- activesupport (= 3.0.12)
19
- builder (~> 2.1.2)
20
- erubis (~> 2.6.6)
21
- i18n (~> 0.5.0)
22
- rack (~> 1.2.5)
23
- rack-mount (~> 0.6.14)
24
- rack-test (~> 0.5.7)
25
- tzinfo (~> 0.3.23)
26
- activemodel (3.0.12)
27
- activesupport (= 3.0.12)
28
- builder (~> 2.1.2)
29
- i18n (~> 0.5.0)
30
- activerecord (3.0.12)
31
- activemodel (= 3.0.12)
32
- activesupport (= 3.0.12)
33
- arel (~> 2.0.10)
34
- tzinfo (~> 0.3.23)
35
- activeresource (3.0.12)
36
- activemodel (= 3.0.12)
37
- activesupport (= 3.0.12)
38
- activesupport (3.0.12)
39
- arel (2.0.10)
40
- builder (2.1.2)
12
+ actionmailer (3.2.5)
13
+ actionpack (= 3.2.5)
14
+ mail (~> 2.4.4)
15
+ actionpack (3.2.5)
16
+ activemodel (= 3.2.5)
17
+ activesupport (= 3.2.5)
18
+ builder (~> 3.0.0)
19
+ erubis (~> 2.7.0)
20
+ journey (~> 1.0.1)
21
+ rack (~> 1.4.0)
22
+ rack-cache (~> 1.2)
23
+ rack-test (~> 0.6.1)
24
+ sprockets (~> 2.1.3)
25
+ activemodel (3.2.5)
26
+ activesupport (= 3.2.5)
27
+ builder (~> 3.0.0)
28
+ activerecord (3.2.5)
29
+ activemodel (= 3.2.5)
30
+ activesupport (= 3.2.5)
31
+ arel (~> 3.0.2)
32
+ tzinfo (~> 0.3.29)
33
+ activeresource (3.2.5)
34
+ activemodel (= 3.2.5)
35
+ activesupport (= 3.2.5)
36
+ activesupport (3.2.5)
37
+ i18n (~> 0.6)
38
+ multi_json (~> 1.0)
39
+ arel (3.0.2)
40
+ builder (3.0.0)
41
41
  daemons (1.1.8)
42
42
  diff-lcs (1.1.3)
43
- erubis (2.6.6)
44
- abstract (>= 1.0.0)
43
+ erubis (2.7.0)
45
44
  eventmachine (1.0.0.beta.4)
46
45
  faye-websocket (0.4.5)
47
46
  eventmachine (>= 0.12.0)
48
- i18n (0.5.0)
49
- json (1.6.5)
50
- mail (2.2.19)
51
- activesupport (>= 2.3.6)
47
+ ffi (1.0.11)
48
+ guard (1.1.1)
49
+ listen (>= 0.4.2)
50
+ thor (>= 0.14.6)
51
+ guard-rspec (1.0.0)
52
+ guard (>= 1.1)
53
+ hike (1.2.1)
54
+ i18n (0.6.0)
55
+ journey (1.0.3)
56
+ json (1.7.3)
57
+ listen (0.4.3)
58
+ rb-fchange (~> 0.0.5)
59
+ rb-fsevent (~> 0.9.1)
60
+ rb-inotify (~> 0.8.8)
61
+ mail (2.4.4)
52
62
  i18n (>= 0.4.0)
53
63
  mime-types (~> 1.16)
54
64
  treetop (~> 1.4.8)
55
65
  mime-types (1.18)
56
66
  multi_json (1.3.6)
57
67
  polyglot (0.3.3)
58
- rack (1.2.5)
59
- rack-mount (0.6.14)
60
- rack (>= 1.0.0)
61
- rack-test (0.5.7)
68
+ rack (1.4.1)
69
+ rack-cache (1.2)
70
+ rack (>= 0.4)
71
+ rack-ssl (1.3.2)
72
+ rack
73
+ rack-test (0.6.1)
62
74
  rack (>= 1.0)
63
- rails (3.0.12)
64
- actionmailer (= 3.0.12)
65
- actionpack (= 3.0.12)
66
- activerecord (= 3.0.12)
67
- activeresource (= 3.0.12)
68
- activesupport (= 3.0.12)
75
+ rails (3.2.5)
76
+ actionmailer (= 3.2.5)
77
+ actionpack (= 3.2.5)
78
+ activerecord (= 3.2.5)
79
+ activeresource (= 3.2.5)
80
+ activesupport (= 3.2.5)
69
81
  bundler (~> 1.0)
70
- railties (= 3.0.12)
71
- railties (3.0.12)
72
- actionpack (= 3.0.12)
73
- activesupport (= 3.0.12)
82
+ railties (= 3.2.5)
83
+ railties (3.2.5)
84
+ actionpack (= 3.2.5)
85
+ activesupport (= 3.2.5)
86
+ rack-ssl (~> 1.3.2)
74
87
  rake (>= 0.8.7)
75
88
  rdoc (~> 3.4)
76
- thor (~> 0.14.4)
89
+ thor (>= 0.14.6, < 2.0)
77
90
  rake (0.9.2.2)
91
+ rb-fchange (0.0.5)
92
+ ffi
93
+ rb-fsevent (0.9.1)
94
+ rb-inotify (0.8.8)
95
+ ffi (>= 0.5.0)
78
96
  rdoc (3.12)
79
97
  json (~> 1.4)
80
- rspec (2.9.0)
81
- rspec-core (~> 2.9.0)
82
- rspec-expectations (~> 2.9.0)
83
- rspec-mocks (~> 2.9.0)
84
- rspec-core (2.9.0)
85
- rspec-expectations (2.9.1)
98
+ rspec (2.10.0)
99
+ rspec-core (~> 2.10.0)
100
+ rspec-expectations (~> 2.10.0)
101
+ rspec-mocks (~> 2.10.0)
102
+ rspec-core (2.10.1)
103
+ rspec-expectations (2.10.0)
86
104
  diff-lcs (~> 1.1.3)
87
- rspec-mocks (2.9.0)
88
- rspec-rails (2.9.0)
105
+ rspec-mocks (2.10.1)
106
+ rspec-rails (2.10.1)
89
107
  actionpack (>= 3.0)
90
108
  activesupport (>= 3.0)
91
109
  railties (>= 3.0)
92
- rspec (~> 2.9.0)
110
+ rspec (~> 2.10.0)
93
111
  simplecov (0.6.4)
94
112
  multi_json (~> 1.0)
95
113
  simplecov-html (~> 0.5.3)
96
114
  simplecov-html (0.5.3)
97
- sqlite3 (1.3.5)
115
+ sprockets (2.1.3)
116
+ hike (~> 1.2)
117
+ rack (~> 1.0)
118
+ tilt (~> 1.1, != 1.3.0)
119
+ sqlite3 (1.3.6)
98
120
  thin (1.3.1)
99
121
  daemons (>= 1.0.9)
100
122
  eventmachine (>= 0.12.6)
101
123
  rack (>= 1.0.0)
102
- thor (0.14.6)
124
+ thor (0.15.2)
125
+ tilt (1.3.3)
103
126
  treetop (1.4.10)
104
127
  polyglot
105
128
  polyglot (>= 0.3.1)
106
- tzinfo (0.3.32)
129
+ tzinfo (0.3.33)
107
130
 
108
131
  PLATFORMS
109
132
  ruby
@@ -111,6 +134,8 @@ PLATFORMS
111
134
  DEPENDENCIES
112
135
  eventmachine (>= 1.0.0.beta.3)
113
136
  faye-websocket
137
+ guard
138
+ guard-rspec
114
139
  rails
115
140
  rake
116
141
  rspec-rails
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/websocket_rails/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/README.md CHANGED
@@ -6,7 +6,7 @@ If you haven't done so yet, check out the [Project Page](http://danknox.github.c
6
6
 
7
7
  ## Overview
8
8
 
9
- Plug and play WebSocket support for ruby on rails. Includes event router for mapping javascript events to controller actions. There is no need for a separate WebSocket server process. Requests to `/websocket` will be passed through to the `ConnectionManager` class which is a simple Rack based WebSocket server developed using the `Faye::WebSocket` library.
9
+ Plug and play WebSocket support for ruby on rails with streaming HTTP fallback for improved cross browser compatibility. Includes event router for mapping javascript events to controller actions. There is no need for a separate WebSocket server process. Requests to `/websocket` will be passed through to the `ConnectionManager` class which is a simple Rack based WebSocket server developed using the `Faye::WebSocket` library.
10
10
 
11
11
  *Important Note*
12
12
 
@@ -69,7 +69,8 @@ The websocket client must connect to `/websocket`. You can connect using the fol
69
69
  ````javascript
70
70
  var conn = new WebSocket("ws://localhost:3000/websocket")
71
71
  conn.onopen = function(evt) {
72
- dispatcher.trigger('new_user',current_user) // Dispatcher not included
72
+ // Example dispatcher located in the assets/ directory
73
+ dispatcher.trigger('new_user',current_user)
73
74
  }
74
75
 
75
76
  conn.onmessage = function(evt) {
@@ -80,7 +81,23 @@ conn.onmessage = function(evt) {
80
81
  }
81
82
  ````
82
83
 
83
- We will be posting a basic javascript event dispatcher soon.
84
+ There are two example dispatchers located in the
85
+ [assets/javascripts](https://github.com/DanKnox/websocket-rails/tree/master/assets/javascripts) directory.
86
+ One for connecting to the server using WebSockets and the other for
87
+ using streaming HTTP. The HTTP dispatcher was built to mimick the
88
+ WebSocket interface so they are completely interchangable. These will
89
+ eventually be merged into one dispatcher which detects which protocol to
90
+ use based on what's available in the browser. Please feel free to submit
91
+ a pull request that accomplishes this.
92
+
93
+ View the source for the dispatchers for example usage or check out the
94
+ [example application](https://github.com/DanKnox/websocket-rails-Example-Project) for a working implementation.
95
+
96
+ *Note on the dispatchers*
97
+
98
+ The example dispatchers are currently meant to be used for reference and
99
+ are not yet included into the Rails asset pipleline. If you want to use
100
+ one, copy it into your local project.
84
101
 
85
102
  ## Controllers
86
103
 
@@ -97,7 +114,7 @@ class ChatController < WebsocketRails::BaseController
97
114
  end
98
115
  ````
99
116
 
100
- The Websocket::BaseController class provides methods for working with the WebSocket connection. Make sure you extend this class for controllers that you are using. The two most important methods are `send_message` and `broadcast_message`. The `send_message` method sends a message to the client that initiated this event, the `broadcast_message` method broadcasts messages to all connected clients. Both methods take two arguments, the event name to trigger on the client, and the message that accompanies it.
117
+ The WebsocketRails::BaseController class provides methods for working with the WebSocket connection. Make sure you extend this class for controllers that you are using. The two most important methods are `send_message` and `broadcast_message`. The `send_message` method sends a message to the client that initiated this event, the `broadcast_message` method broadcasts messages to all connected clients. Both methods take two arguments, the event name to trigger on the client, and the message that accompanies it.
101
118
 
102
119
  ````ruby
103
120
  new_message = {:message => 'this is a message'}
@@ -149,7 +166,7 @@ end
149
166
  If you wish to output an Array of the assigned values in the data store for every connected client, you can use the `each_<key>` method, replacing `<key>` with the hash key that you wish to collect.
150
167
 
151
168
  Given our ongoing chat server example, we could collect all of the current `User` objects like so:
152
-
169
+ d
153
170
  ````ruby
154
171
  data_store[:user] = 'User3'
155
172
  data_store.each_user
@@ -167,7 +184,7 @@ end
167
184
 
168
185
  ## Message Format
169
186
 
170
- The message can be a string, hash, or array. The message is serialized as JSON before being sent to the client. The message arrives at the client as a two element serialized array with the `event_name` string as the first element and the message object you passed to the `message` parameter of the `send_message` method as the second element.
187
+ The message can be a string, hash, or array. The message is serialized as JSON before being sent to the client. The message arrives at the client as a three element serialized array with the `client_id` as the first element,`event_name` string as the second element, and the message object you passed to the `message` parameter of the `send_message` method as the third element.
171
188
 
172
189
  If you executed this code in your controller:
173
190
 
@@ -179,7 +196,7 @@ send_message :new_message, new_message
179
196
  The message that arrives on the client would look like:
180
197
 
181
198
  ````javascript
182
- ['new_message',{message: 'this is a message'}]
199
+ ['70291412510420','new_message',{message: 'this is a message'}]
183
200
  ````
184
201
 
185
202
  ## Development
@@ -188,4 +205,4 @@ This gem is created and maintained by Dan Knox and Kyle Whalen under the MIT Lic
188
205
 
189
206
  Brought to you by:
190
207
 
191
- Three Dot Loft LLC
208
+ Three Dot Loft LLC
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Example HTTP event dispatcher.
3
+ *
4
+ * Setting up the dispatcher:
5
+ * var dispatcher = new ServerEventsDispatcher()
6
+ * dispatcher.onopen(function() {
7
+ * // trigger a server event immediately after opening connection
8
+ * dispatcher.trigger('new_user',{user_name: 'guest'})
9
+ * })
10
+ *
11
+ * Triggering a new event on the server
12
+ * dispatcher.trigger('event_name',object_to_be_serialized_to_json)
13
+ *
14
+ * Listening for new events from the server
15
+ * dispatcher.bind('event_name', function(data) {
16
+ * alert(data.user_name)
17
+ * })
18
+ */
19
+ var ServerEventsDispatcher = function(){
20
+ var conn = new XMLHttpRequest(),
21
+ open_handler = function(){},
22
+ loaded = false,
23
+ lastPos = 0,
24
+ client_id = '';
25
+
26
+ conn.onreadystatechange = function() {
27
+ if (conn.readyState == 3) {
28
+ var data = conn.responseText.substring(lastPos);
29
+ lastPos = conn.responseText.length;
30
+ var json_data = JSON.parse(data),
31
+ id = json_data[0],
32
+ event_name = json_data[1],
33
+ message = json_data[2];
34
+
35
+ client_id = id
36
+
37
+ if (loaded == false) {
38
+ open_handler();
39
+ loaded = true
40
+ }
41
+ console.log(json_data)
42
+ dispatch(event_name, message)
43
+ }
44
+ }
45
+ conn.open("GET","/websocket",true)
46
+ conn.send()
47
+
48
+ var callbacks = {}
49
+
50
+ this.bind = function(event_name, callback) {
51
+ callbacks[event_name] = callbacks[event_name] || [];
52
+ callbacks[event_name].push(callback)
53
+ }
54
+
55
+ this.trigger = function(event_name, data) {
56
+ var payload = JSON.stringify([event_name,data])
57
+ $.ajax({
58
+ type: 'POST',
59
+ url: '/websocket',
60
+ data: {client_id: client_id, data: payload},
61
+ success: function(){console.log('success');}
62
+ });
63
+ return this;
64
+ }
65
+
66
+ this.onopen = function(handler) {
67
+ open_handler = handler
68
+ }
69
+
70
+ var dispatch = function(event_name, message) {
71
+ var chain = callbacks[event_name]
72
+ if (typeof chain == 'undefined') return;
73
+ for(var i = 0; i < chain.length; i++) {
74
+ chain[i]( message )
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Example WebSocket event dispatcher.
3
+ *
4
+ * Setting up the dispatcher:
5
+ * var dispatcher = new ServerEventsDispatcher()
6
+ * dispatcher.onopen(function() {
7
+ * // trigger a server event immediately after opening connection
8
+ * dispatcher.trigger('new_user',{user_name: 'guest'})
9
+ * })
10
+ *
11
+ * Triggering a new event on the server
12
+ * dispatcher.trigger('event_name',object_to_be_serialized_to_json)
13
+ *
14
+ * Listening for new events from the server
15
+ * dispatcher.bind('event_name', function(data) {
16
+ * alert(data.user_name)
17
+ * })
18
+ */
19
+ var ServerEventsDispatcher = function(){
20
+ var conn = new WebSocket("ws://localhost:3000/websocket"),
21
+ open_handler = function(){},
22
+ callbacks = {},
23
+ client_id = '';
24
+
25
+ this.bind = function(event_name, callback) {
26
+ callbacks[event_name] = callbacks[event_name] || [];
27
+ callbacks[event_name].push(callback)
28
+ }
29
+
30
+ this.trigger = function(event_name, data) {
31
+ var payload = JSON.stringify([event_name,data])
32
+ conn.send( payload )
33
+ return this;
34
+ }
35
+
36
+ conn.onopen = function(evt) {
37
+ open_handler()
38
+ }
39
+ this.onopen = function(handler) {
40
+ open_handler = handler
41
+ }
42
+
43
+ conn.onmessage = function(evt) {
44
+ var data = JSON.parse(evt.data),
45
+ id = data[0],
46
+ event_name = data[1],
47
+ message = data[2];
48
+
49
+ client_id = id
50
+ console.log(data)
51
+ dispatch(event_name, message)
52
+ }
53
+
54
+ conn.onclose = function(evt) {
55
+ dispatch('connection_closed', '')
56
+ }
57
+
58
+ var dispatch = function(event_name, message) {
59
+ var chain = callbacks[event_name]
60
+ if (typeof chain == 'undefined') return;
61
+ for(var i = 0; i < chain.length; i++) {
62
+ chain[i]( message )
63
+ }
64
+ }
65
+ }
@@ -23,5 +23,9 @@ require 'websocket_rails/dispatcher'
23
23
  require 'websocket_rails/events'
24
24
  require 'websocket_rails/base_controller'
25
25
 
26
+ require 'websocket_rails/connection_adapters'
27
+ require 'websocket_rails/connection_adapters/http'
28
+ require 'websocket_rails/connection_adapters/web_socket'
29
+
26
30
  ::Thin::Server.send( :remove_const, 'DEFAULT_TIMEOUT' )
27
31
  ::Thin::Server.const_set( 'DEFAULT_TIMEOUT', 0 )
@@ -60,7 +60,7 @@ module WebsocketRails
60
60
  # for each currently active connection but can not be used to associate a client between
61
61
  # multiple connection attempts.
62
62
  def client_id
63
- connection.object_id
63
+ connection.id
64
64
  end
65
65
 
66
66
  # The current message that was passed from the client when the event was initiated. The
@@ -77,12 +77,12 @@ module WebsocketRails
77
77
  # # Will arrive on the client as JSON string like the following:
78
78
  # # ['new_message',{message: 'new message for the client'}]
79
79
  def send_message(event, message)
80
- @_dispatcher.send_message event.to_s, message, connection if @_dispatcher.respond_to?(:send_message)
80
+ @_dispatcher.send_message client_id, event.to_s, message, connection if @_dispatcher.respond_to?(:send_message)
81
81
  end
82
82
 
83
83
  # Broadcasts a message to all connected clients. See {#send_message} for message passing details.
84
84
  def broadcast_message(event, message)
85
- @_dispatcher.broadcast_message event.to_s, message if @_dispatcher.respond_to?(:broadcast_message)
85
+ @_dispatcher.broadcast_message client_id, event.to_s, message if @_dispatcher.respond_to?(:broadcast_message)
86
86
  end
87
87
 
88
88
  # Provides access to the {DataStore} for the current controller. The {DataStore} provides convenience
@@ -107,4 +107,4 @@ module WebsocketRails
107
107
  end
108
108
 
109
109
  end
110
- end
110
+ end
@@ -0,0 +1,52 @@
1
+ module WebsocketRails
2
+ module ConnectionAdapters
3
+
4
+ attr_reader :adapters
5
+ module_function :adapters
6
+
7
+ def self.register_adapter(adapter)
8
+ @adapters ||= []
9
+ @adapters.unshift adapter
10
+ end
11
+
12
+ def self.establish_connection(env)
13
+ adapter = adapters.detect { |a| a.accepts?( env ) } || return
14
+ adapter.new( env )
15
+ end
16
+
17
+ class Base
18
+
19
+ ADAPTER_EVENTS = [:onmessage, :onerror, :onclose]
20
+
21
+ def self.inherited(adapter)
22
+ ConnectionAdapters.register_adapter( adapter )
23
+ end
24
+
25
+ def initialize(env)
26
+ @env = env
27
+ end
28
+
29
+ ADAPTER_EVENTS.each do |adapter_event|
30
+ define_method "#{adapter_event}" do |event=nil|
31
+ instance_variable_get( "@#{adapter_event}" ).call( event )
32
+ end
33
+ define_method "#{adapter_event}=" do |block=nil|
34
+ instance_variable_set( "@#{adapter_event}", block )
35
+ end
36
+ end
37
+
38
+ def send(message)
39
+ raise NotImplementedError, "Override this method in the connection specific adapter class"
40
+ end
41
+
42
+ def rack_response
43
+ [ -1, {}, [] ]
44
+ end
45
+
46
+ def id
47
+ object_id.to_i
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,105 @@
1
+ module WebsocketRails
2
+ module ConnectionAdapters
3
+ class Http < Base
4
+ TERM = "\r\n".freeze
5
+ TAIL = "0#{TERM}#{TERM}".freeze
6
+
7
+ def self.accepts?(env)
8
+ true
9
+ end
10
+
11
+ attr_accessor :headers
12
+
13
+ def initialize(env)
14
+ super
15
+ @body = DeferrableBody.new
16
+ @headers = Hash.new
17
+ @headers['Content-Type'] = 'text/json'
18
+ @headers['Transfer-Encoding'] = 'chunked'
19
+
20
+ define_deferrable_callbacks
21
+ EM.next_tick { @env['async.callback'].call [200, @headers, @body] }
22
+ end
23
+
24
+ def send(message)
25
+ @body.chunk encode_chunk( message )
26
+ end
27
+
28
+ private
29
+
30
+ def define_deferrable_callbacks
31
+ @body.callback do |event|
32
+ onclose(event)
33
+ end
34
+ @body.errback do |event|
35
+ onclose(event)
36
+ end
37
+ end
38
+
39
+ # From [Rack::Stream](https://github.com/intridea/rack-stream)
40
+ def encode_chunk(c)
41
+ return nil if c.nil?
42
+ # hack to work with Rack::File for now, should not TE chunked
43
+ # things that aren't strings or respond to bytesize
44
+ c = ::File.read(c.path) if c.kind_of?(Rack::File)
45
+ size = Rack::Utils.bytesize(c)
46
+ return nil if size == 0
47
+ c.dup.force_encoding(Encoding::BINARY) if c.respond_to?(:force_encoding)
48
+ puts "Chunking:: #{c}"
49
+ puts "Chunking:: #{size.to_s(16)}#{TERM}#{c}#{TERM}"
50
+ [size.to_s(16), TERM, c, TERM].join
51
+ end
52
+
53
+ # From [thin_async](https://github.com/macournoyer/thin_async)
54
+ class DeferrableBody
55
+ include EM::Deferrable
56
+
57
+ # @param chunks - object that responds to each. holds initial chunks of content
58
+ def initialize(chunks = [])
59
+ @queue = []
60
+ chunks.each {|c| chunk(c)}
61
+ end
62
+
63
+ # Enqueue a chunk of content to be flushed to stream at a later time
64
+ def chunk(*chunks)
65
+ @queue += chunks
66
+ schedule_dequeue
67
+ end
68
+
69
+ # When rack attempts to iterate over `body`, save the block,
70
+ # and execute at a later time when `@queue` has elements
71
+ def each(&blk)
72
+ @body_callback = blk
73
+ schedule_dequeue
74
+ end
75
+
76
+ def empty?
77
+ @queue.empty?
78
+ end
79
+
80
+ def close!(flush = true)
81
+ EM.next_tick {
82
+ if !flush || empty?
83
+ succeed
84
+ else
85
+ schedule_dequeue
86
+ close!(flush)
87
+ end
88
+ }
89
+ end
90
+
91
+ private
92
+
93
+ def schedule_dequeue
94
+ return unless @body_callback
95
+ EM.next_tick do
96
+ next unless c = @queue.shift
97
+ @body_callback.call(c)
98
+ schedule_dequeue unless empty?
99
+ end
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,28 @@
1
+ module WebsocketRails
2
+ module ConnectionAdapters
3
+ class WebSocket < Base
4
+
5
+ extend Forwardable
6
+
7
+ def self.accepts?(env)
8
+ ::Faye::WebSocket.websocket?( env )
9
+ end
10
+
11
+ def self.delegated_methods
12
+ setter_methods = ADAPTER_EVENTS.map {|e| "#{e}=".to_sym }
13
+ setter_methods + ADAPTER_EVENTS
14
+ end
15
+ def_delegators :@connection, *delegated_methods
16
+
17
+ def initialize(env)
18
+ super
19
+ @connection = ::Faye::WebSocket.new( env )
20
+ end
21
+
22
+ def send(message)
23
+ @connection.send message
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -3,30 +3,68 @@ require 'rack'
3
3
  require 'thin'
4
4
 
5
5
  module WebsocketRails
6
+ class InvalidConnection < StandardError; end
6
7
  # The +ConnectionManager+ class implements the core Rack application that handles
7
8
  # incoming WebSocket connections.
8
9
  class ConnectionManager
9
10
 
10
11
  # Contains an Array of currently open Faye::WebSocket connections.
11
12
  # @return [Array]
12
- attr_accessor :connections
13
+ attr_reader :connections
14
+
15
+ # Contains the {Dispatcher} instance for the active server.
16
+ # @return [Dispatcher]
17
+ attr_reader :dispatcher
13
18
 
14
19
  def initialize
15
20
  @connections = []
16
21
  @dispatcher = Dispatcher.new( self )
17
22
  end
18
23
 
19
- # Opens a new Faye::WebSocket connection using the Rack env Hash. New connections
20
- # dispatch the 'client_connected' event through the {Dispatcher} and are then
21
- # stored in the active {connections} Array. An Async response is returned to
22
- # signify to the web server that the connection will remain opened. Invalid
23
- # connections return an HTTP 400 Bad Request response to the client.
24
- def call(env)
25
- return invalid_connection_attempt unless Faye::WebSocket.websocket?( env )
26
- connection = Faye::WebSocket.new( env )
24
+ # Primary entry point for the Rack application
25
+ def call(env)
26
+ request = Rack::Request.new( env )
27
+ if request.post?
28
+ response = parse_incoming_event( request.params )
29
+ else
30
+ response = open_connection( env )
31
+ end
32
+ response
33
+ end
34
+
35
+ # Used to broadcast a message to all connected clients. This method should never
36
+ # be called directly. Instead, users should use {BaseController#broadcast_message}
37
+ # and {BaseController#send_message} in their applications.
38
+ def broadcast_message(message)
39
+ @connections.map do |connection|
40
+ connection.send message
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def parse_incoming_event(params)
47
+ connection = find_connection_by_id params["client_id"]
48
+ data = params["data"]
49
+ @dispatcher.receive( data, connection )
50
+ [200,{'Content-Type' => 'text/plain'},['success']]
51
+ rescue InvalidConnection
52
+ [400,{'Content-Type' => 'text/plain'},['invalid connection']]
53
+ end
54
+
55
+ def find_connection_by_id(id)
56
+ connections.detect { |connection| connection.id == id.to_i } || (raise InvalidConnection)
57
+ end
58
+
59
+ # Opens a persistent connection using the appropriate {ConnectionAdapter}. Stores
60
+ # active connections in the {connections} array.
61
+ def open_connection(env)
62
+ connection = ConnectionAdapters.establish_connection( env )
63
+ return invalid_connection_attempt unless connection
27
64
 
28
65
  puts "Client #{connection} connected\n"
29
66
  @dispatcher.dispatch( 'client_connected', {}, connection )
67
+ @dispatcher.send_message( connection.id, :connection_open, {}, connection )
30
68
 
31
69
  connection.onmessage = lambda do |event|
32
70
  @dispatcher.receive( event.data, connection )
@@ -48,21 +86,10 @@ module WebsocketRails
48
86
  connections << connection
49
87
  connection.rack_response
50
88
  end
51
-
52
- # Used to broadcast a message to all connected clients. This method should never
53
- # be called directly. Instead, users should use {BaseController#broadcast_message}
54
- # and {BaseController#send_message} in their applications.
55
- def broadcast_message(message)
56
- @connections.map do |connection|
57
- connection.send message
58
- end
59
- end
60
-
61
- private
62
-
89
+
63
90
  def invalid_connection_attempt
64
91
  [400,{'Content-Type' => 'text/plain'}, ['Connection was not a valid WebSocket connection']]
65
92
  end
66
93
 
67
94
  end
68
- end
95
+ end
@@ -22,12 +22,12 @@ module WebsocketRails
22
22
  dispatch( event_name, data, connection )
23
23
  end
24
24
 
25
- def send_message(event_name,data,connection)
26
- connection.send encoded_message( event_name, data )
25
+ def send_message(client_id,event_name,data,connection)
26
+ connection.send encoded_message( client_id, event_name, data )
27
27
  end
28
28
 
29
- def broadcast_message(event_name,data)
30
- @connection_manager.broadcast_message encoded_message( event_name, data )
29
+ def broadcast_message(client_id,event_name,data)
30
+ @connection_manager.broadcast_message encoded_message( client_id, event_name, data )
31
31
  end
32
32
 
33
33
  def dispatch(event_name,message,connection)
@@ -42,9 +42,9 @@ module WebsocketRails
42
42
  }.resume
43
43
  end
44
44
 
45
- def encoded_message(event_name,data)
46
- [event_name, data].to_json
45
+ def encoded_message(client_id,event_name,data)
46
+ [client_id, event_name, data].to_json
47
47
  end
48
48
 
49
49
  end
50
- end
50
+ end
@@ -1,3 +1,3 @@
1
1
  module WebsocketRails
2
- VERSION = "0.1.1"
3
- end
2
+ VERSION = "0.1.2"
3
+ end
@@ -26,6 +26,10 @@ class ChatController < WebsocketRails::BaseController
26
26
  # do something when a client connects
27
27
  end
28
28
 
29
+ def error_occurred
30
+ # do something when an error occurs
31
+ end
32
+
29
33
  def new_message
30
34
  puts "Message from UID: #{client_id}\n"
31
35
  @message_counter += 1
@@ -1,38 +1,91 @@
1
1
  require 'spec_helper'
2
+ require 'support/mock_web_socket'
2
3
 
3
4
  module WebsocketRails
4
- describe ConnectionManager do
5
+ describe ConnectionManager, "integration test" do
5
6
 
6
7
  def define_test_events
7
8
  WebsocketRails.route_block = nil
8
9
  WebsocketRails::Events.describe_events do
9
10
  subscribe :client_connected, to: ChatController, with_method: :new_user
10
11
  subscribe :change_username, to: ChatController, with_method: :change_username
12
+ subscribe :client_error, to: ChatController, with_method: :error_occurred
13
+ subscribe :client_disconnected, to: ChatController, with_method: :delete_user
11
14
  end
12
15
  end
13
16
 
14
- before(:all) { define_test_events }
15
-
16
- let(:env) { Hash.new }
17
- let(:socket) { MockWebSocket.new }
18
-
19
- before(:each) do
20
- Faye::WebSocket.stub(:new).and_return(MockWebSocket.new)
21
- @server = ConnectionManager.new
22
- end
23
-
24
- context "new connections" do
25
- it "should execute the controller action associated with the 'client_connected' event" do
26
- ChatController.any_instance.should_receive(:new_user)
27
- define_test_events
28
- @server.call( env )
29
- #ChatController.new.send(:new_user)
17
+ before(:all) {
18
+ define_test_events
19
+ if defined?(ConnectionAdapters::Test)
20
+ ConnectionAdapters.adapters.delete( ConnectionAdapters::Test )
21
+ end
22
+ }
23
+
24
+ shared_examples "an evented rack server" do
25
+ context "new connections" do
26
+ it "should execute the controller action associated with the 'client_connected' event" do
27
+ ChatController.any_instance.should_receive(:new_user)
28
+ @server.call( env )
29
+ end
30
30
  end
31
31
 
32
- it "should have a ChatController present" do
33
- ChatController.new.should be_present
32
+ context "active connections" do
33
+ context "new message from client" do
34
+ let(:test_message) { ['change_username',{user_name: 'Joe User'}] }
35
+ let(:encoded_message) { test_message.to_json }
36
+
37
+ before(:each) { MockEvent = Struct.new(:data) }
38
+
39
+ it "should execute the controller action associated with the received event" do
40
+ mock_event = MockEvent.new( encoded_message )
41
+ ChatController.any_instance.should_receive(:change_username)
42
+ @server.call( env )
43
+ socket.onmessage( mock_event )
44
+ end
45
+ end
46
+
47
+ context "client error" do
48
+ it "should execute the controller action associated with the 'client_error' event" do
49
+ ChatController.any_instance.should_receive(:error_occurred)
50
+ @server.call( env )
51
+ socket.onerror
52
+ end
53
+ end
54
+
55
+ context "client disconnects" do
56
+ it "should execute the controller action associated with the 'client_disconnected' event" do
57
+ ChatController.any_instance.should_receive(:delete_user)
58
+ @server.call( env )
59
+ socket.onclose
60
+ end
61
+ end
34
62
  end
35
63
  end
36
-
64
+
65
+ let(:env) { Rack::MockRequest.env_for('/websocket') }
66
+
67
+ context "WebSocket Adapter" do
68
+ let(:socket) { MockWebSocket.new }
69
+
70
+ before do
71
+ ::Faye::WebSocket.stub(:websocket?).and_return(true)
72
+ ::Faye::WebSocket.stub(:new).and_return(socket)
73
+ @server = ConnectionManager.new
74
+ end
75
+
76
+ it_behaves_like 'an evented rack server'
77
+ end
78
+
79
+ describe "HTTP Adapter" do
80
+ let(:socket) { ConnectionAdapters::Http.new( env ) }
81
+
82
+ before do
83
+ ConnectionAdapters.stub(:establish_connection).and_return(socket)
84
+ @server = ConnectionManager.new
85
+ end
86
+
87
+ it_behaves_like 'an evented rack server'
88
+ end
89
+
37
90
  end
38
- end
91
+ end
data/spec/spec_helper.rb CHANGED
@@ -7,7 +7,6 @@ require File.expand_path("../../spec/dummy/config/environment", __FILE__)
7
7
  require 'rspec/rails'
8
8
  require 'rspec/autorun'
9
9
  require 'thin'
10
- #require 'vendor/em-rspec/lib/em-rspec'
11
10
 
12
11
  $:.push File.expand_path("../../lib", __FILE__)
13
12
  require 'websocket-rails'
@@ -1,3 +1,5 @@
1
+ require 'spec_helper'
2
+
1
3
  module WebsocketRails
2
4
 
3
5
  class MockWebSocket
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ module WebsocketRails
4
+ module ConnectionAdapters
5
+ describe Http do
6
+
7
+ let(:env) { Rack::MockRequest.env_for('/websocket') }
8
+
9
+ subject { Http.new( env ) }
10
+
11
+ it "should be a subclass of ConnectionAdapters::Base" do
12
+ subject.class.superclass.should == ConnectionAdapters::Base
13
+ end
14
+
15
+ it "should set the Content-Length header to text/json" do
16
+ subject.headers['Content-Type'].should == "text/json"
17
+ end
18
+
19
+ it "should set the Transfer-Encoding header to chunked" do
20
+ subject.headers['Transfer-Encoding'].should == "chunked"
21
+ end
22
+
23
+ context "#encode_chunk" do
24
+ it "should properly encode strings" do
25
+ subject.__send__(:encode_chunk,"test").should == "4\r\ntest\r\n"
26
+ end
27
+ end
28
+
29
+ context "adapter methods" do
30
+ before do
31
+ @body = double('DeferrableBody').as_null_object
32
+ Http::DeferrableBody.stub(:new).and_return(@body)
33
+ end
34
+
35
+ context "#define_deferrable_callbacks" do
36
+ it "should define a callback for :succeeded" do
37
+ @body.should_receive(:callback)
38
+ subject
39
+ end
40
+
41
+ it "should define a callback for :failed" do
42
+ @body.should_receive(:errback)
43
+ subject
44
+ end
45
+ end
46
+
47
+ context "#send" do
48
+ it "should encode the message before sending" do
49
+ subject.should_receive(:encode_chunk).with('test message')
50
+ subject.send 'test message'
51
+ end
52
+
53
+ it "should enqueue the message on DeferrableBody" do
54
+ encoded_message = subject.__send__(:encode_chunk,'test message')
55
+ @body.should_receive(:chunk).with(encoded_message)
56
+ subject.send 'test message'
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'support/mock_web_socket'
3
+
4
+ module WebsocketRails
5
+ module ConnectionAdapters
6
+ describe WebSocket do
7
+
8
+ before do
9
+ @socket = MockWebSocket.new
10
+ Faye::WebSocket.stub(:new).and_return(@socket)
11
+ @adapter = WebSocket.new( Hash.new )
12
+ end
13
+
14
+ WebSocket::ADAPTER_EVENTS.each do |event|
15
+ it "should delegate ##{event} and ##{event}= to the Faye::WebSocket object" do
16
+ @socket.should_receive(event)
17
+ @socket.should_receive("#{event}=".to_sym)
18
+
19
+ @adapter.__send__( "#{event}=".to_sym, Proc.new {|e| true } )
20
+ @adapter.__send__( event )
21
+ end
22
+ end
23
+
24
+ context "#send" do
25
+ it "should send the message to the websocket connection" do
26
+ @socket.should_receive(:send).with(:message)
27
+ @adapter.send :message
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ module WebsocketRails
4
+
5
+ class ConnectionAdapters::Test < ConnectionAdapters::Base
6
+ def self.accepts?(env)
7
+ true
8
+ end
9
+ end
10
+
11
+ describe ConnectionAdapters do
12
+
13
+ let(:env) { Rack::MockRequest.env_for('/websocket') }
14
+
15
+ context ".register_adapter" do
16
+ it "should store a reference to the adapter in the adapters array" do
17
+ ConnectionAdapters.register_adapter( ConnectionAdapters::Test )
18
+ ConnectionAdapters.adapters.include?( ConnectionAdapters::Test ).should be_true
19
+ end
20
+ end
21
+
22
+ context ".establish_connection" do
23
+ it "should return the correct connection adapter instance" do
24
+ adapter = ConnectionAdapters.establish_connection( env )
25
+ adapter.class.should == ConnectionAdapters::Test
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ module ConnectionAdapters
32
+ describe Base do
33
+
34
+ let(:env) { Rack::MockRequest.env_for('/websocket') }
35
+
36
+ subject { Base.new( env ) }
37
+
38
+ context "new adapters" do
39
+ it "should register themselves in the adapters array when inherited" do
40
+ adapter = Class.new( ConnectionAdapters::Base )
41
+ ConnectionAdapters.adapters.include?( adapter ).should be_true
42
+ end
43
+
44
+ Base::ADAPTER_EVENTS.each do |event|
45
+ it "should define accessor methods for #{event}" do
46
+ proc = lambda { |event| true }
47
+ subject.__send__("#{event}=".to_sym,proc)
48
+ subject.__send__(event).should == true
49
+ end
50
+ end
51
+ end
52
+
53
+ context "#send" do
54
+ it "should raise a NotImplementedError exception" do
55
+ expect { subject.send :message }.to raise_exception( NotImplementedError )
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -1,22 +1,25 @@
1
1
  require 'spec_helper'
2
- require 'support/mock_web_socket'
3
2
 
4
3
  module WebsocketRails
5
4
  describe ConnectionManager do
6
-
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ @app ||= ConnectionManager.new
9
+ end
10
+
7
11
  def open_connection
8
- subject.call(@env)
12
+ subject.call(env)
9
13
  end
10
14
 
11
15
  let(:connections) { subject.connections }
16
+ let(:env) { Rack::MockRequest.env_for('/websocket') }
12
17
 
13
18
  before(:each) do
14
- Faye::WebSocket.stub(:websocket?).and_return(true)
15
- @mock_socket = MockWebSocket.new
16
- Faye::WebSocket.stub(:new).and_return(@mock_socket)
19
+ @mock_socket = ConnectionAdapters::Base.new(env)
20
+ ConnectionAdapters.stub(:establish_connection).and_return(@mock_socket)
17
21
  @dispatcher = double('dispatcher').as_null_object
18
22
  Dispatcher.stub(:new).and_return(@dispatcher)
19
- @env = {}
20
23
  end
21
24
 
22
25
  context "new connections" do
@@ -40,11 +43,23 @@ module WebsocketRails
40
43
  it "should return an Async Rack response" do
41
44
  open_connection.should == [ -1, {}, [] ]
42
45
  end
43
- end
46
+ end
47
+
48
+ context "new POST event" do
49
+ before(:each) do
50
+ @mock_http = ConnectionAdapters::Http.new(env)
51
+ app.connections << @mock_http
52
+ end
53
+
54
+ it "should receive the new event for the correct connection" do
55
+ @dispatcher.should_receive(:receive).with('data',@mock_http)
56
+ post '/websocket', {:client_id => @mock_http.id, :data => 'data'}
57
+ end
58
+ end
44
59
 
45
60
  context "open connections" do
46
61
  before(:each) do
47
- Faye::WebSocket.stub(:new).and_return(MockWebSocket.new,MockWebSocket.new,@mock_socket,MockWebSocket.new)
62
+ ConnectionAdapters.stub(:establish_connection).and_return(@mock_socket,ConnectionAdapters::Base.new(env))
48
63
  4.times { open_connection }
49
64
  end
50
65
 
@@ -100,7 +115,7 @@ module WebsocketRails
100
115
 
101
116
  context "invalid connections" do
102
117
  before(:each) do
103
- Faye::WebSocket.stub(:websocket?).and_return(false)
118
+ ConnectionAdapters.stub(:establish_connection).and_return(false)
104
119
  end
105
120
 
106
121
  it "should return a 400 bad request error code" do
@@ -108,4 +123,4 @@ module WebsocketRails
108
123
  end
109
124
  end
110
125
  end
111
- end
126
+ end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'support/mock_web_socket'
2
3
  require 'json'
3
4
 
4
5
  module WebsocketRails
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: websocket-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-06-02 00:00:00.000000000Z
14
+ date: 2012-06-10 00:00:00.000000000Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rack
@@ -138,13 +138,19 @@ files:
138
138
  - .travis.yml
139
139
  - Gemfile
140
140
  - Gemfile.lock
141
+ - Guardfile
141
142
  - MIT-LICENSE
142
143
  - README.md
143
144
  - Rakefile
145
+ - assets/javascripts/http_dispatcher.js
146
+ - assets/javascripts/websocket_dispatcher.js
144
147
  - bin/thin-socketrails
145
148
  - config/routes.rb
146
149
  - lib/websocket-rails.rb
147
150
  - lib/websocket_rails/base_controller.rb
151
+ - lib/websocket_rails/connection_adapters.rb
152
+ - lib/websocket_rails/connection_adapters/http.rb
153
+ - lib/websocket_rails/connection_adapters/web_socket.rb
148
154
  - lib/websocket_rails/connection_manager.rb
149
155
  - lib/websocket_rails/data_store.rb
150
156
  - lib/websocket_rails/dispatcher.rb
@@ -176,7 +182,6 @@ files:
176
182
  - spec/dummy/log/development.log
177
183
  - spec/dummy/log/production.log
178
184
  - spec/dummy/log/server.log
179
- - spec/dummy/log/test.log
180
185
  - spec/dummy/public/404.html
181
186
  - spec/dummy/public/422.html
182
187
  - spec/dummy/public/500.html
@@ -192,6 +197,9 @@ files:
192
197
  - spec/integration/connection_manager_spec.rb
193
198
  - spec/spec_helper.rb
194
199
  - spec/support/mock_web_socket.rb
200
+ - spec/unit/connection_adapters/http_spec.rb
201
+ - spec/unit/connection_adapters/web_socket_spec.rb
202
+ - spec/unit/connection_adapters_spec.rb
195
203
  - spec/unit/connection_manager_spec.rb
196
204
  - spec/unit/data_store_spec.rb
197
205
  - spec/unit/dispatcher_spec.rb
@@ -211,7 +219,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
211
219
  version: '0'
212
220
  segments:
213
221
  - 0
214
- hash: -481953751813438507
222
+ hash: 2040378908172396900
215
223
  required_rubygems_version: !ruby/object:Gem::Requirement
216
224
  none: false
217
225
  requirements:
@@ -220,7 +228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
228
  version: '0'
221
229
  segments:
222
230
  - 0
223
- hash: -481953751813438507
231
+ hash: 2040378908172396900
224
232
  requirements: []
225
233
  rubyforge_project:
226
234
  rubygems_version: 1.8.19
File without changes