websocket-rails 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG.md +34 -0
  2. data/Gemfile +1 -0
  3. data/README.md +34 -34
  4. data/assets/javascripts/http_dispatcher.js +6 -5
  5. data/assets/javascripts/websocket_dispatcher.js +7 -6
  6. data/lib/websocket-rails.rb +22 -2
  7. data/lib/websocket_rails/base_controller.rb +24 -8
  8. data/lib/websocket_rails/connection_adapters.rb +43 -16
  9. data/lib/websocket_rails/connection_adapters/http.rb +14 -10
  10. data/lib/websocket_rails/connection_adapters/web_socket.rb +13 -12
  11. data/lib/websocket_rails/connection_manager.rb +19 -47
  12. data/lib/websocket_rails/dispatcher.rb +39 -33
  13. data/lib/websocket_rails/event.rb +71 -0
  14. data/lib/websocket_rails/event_map.rb +120 -0
  15. data/lib/websocket_rails/version.rb +1 -1
  16. data/spec/dummy/config/initializers/events.rb +2 -2
  17. data/spec/dummy/{public/stylesheets/.gitkeep → log/development.log} +0 -0
  18. data/spec/dummy/log/production.log +0 -0
  19. data/spec/dummy/log/server.log +0 -0
  20. data/spec/dummy/log/test.log +454 -0
  21. data/spec/integration/connection_manager_spec.rb +29 -13
  22. data/spec/spec_helper.rb +3 -3
  23. data/spec/support/helper_methods.rb +29 -0
  24. data/spec/support/mock_web_socket.rb +6 -2
  25. data/spec/unit/connection_adapters/http_spec.rb +1 -3
  26. data/spec/unit/connection_adapters/web_socket_spec.rb +1 -11
  27. data/spec/unit/connection_adapters_spec.rb +51 -19
  28. data/spec/unit/connection_manager_spec.rb +22 -46
  29. data/spec/unit/dispatcher_spec.rb +56 -25
  30. data/spec/unit/event_map_spec.rb +96 -0
  31. data/spec/unit/event_spec.rb +99 -0
  32. metadata +23 -25
  33. data/.gitignore +0 -11
  34. data/.rspec +0 -2
  35. data/.travis.yml +0 -3
  36. data/Gemfile.lock +0 -144
  37. data/Guardfile +0 -9
  38. data/lib/websocket_rails/events.rb +0 -53
  39. data/spec/unit/events_spec.rb +0 -70
  40. data/websocket-rails.gemspec +0 -26
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # WebsocketRails Change Log
2
+
3
+ ## Version 0.1.3
4
+
5
+ June 22 2012
6
+
7
+ * Added support for namespaced events.
8
+ * Improved event machine scheduling for action processing.
9
+ * Made a client's connection ID private.
10
+ * Bugfixes in the JavaScript event dispatchers.
11
+
12
+ ## Version 0.1.2
13
+
14
+ June 10 2012
15
+
16
+ * Added streaming HTTP support as a fallback from WebSockets.
17
+ * Added example JavaScript event dispatchers.
18
+
19
+ ## Version 0.1.1
20
+
21
+ June 2 2012
22
+
23
+ * Created project home page.
24
+ * Improved test coverage and cleaned up the internals.
25
+
26
+ ## Version 0.1.0
27
+
28
+ April 14 2012
29
+
30
+ * Complete project rewrite.
31
+ * Removed websocket-rack dependency.
32
+ * Enhanced documentation.
33
+ * Added event observers in WebsocketRail Controllers.
34
+ * First stable release!
data/Gemfile CHANGED
@@ -6,5 +6,6 @@ gem "rspec-rails"
6
6
  gem "eventmachine", ">= 1.0.0.beta.3"
7
7
  gem "faye-websocket"
8
8
  gem "simplecov"
9
+ gem "ruby_gntp"
9
10
  gem "guard"
10
11
  gem "guard-rspec"
data/README.md CHANGED
@@ -41,10 +41,16 @@ There are two built in events that are fired automatically by the dispatcher. Th
41
41
 
42
42
  You can subscribe multiple controllers and actions to the same event to provide very clean event handling logic. The new message will be available in each controller using the `message` method discussed in the *Controllers* section below. The example event router below demonstrates subscribing to the `:new_message` event with one controller action to rebroadcast the message out to all connected clients and another controller action to log the message to a database.
43
43
 
44
+ The Event Map now supports namespacing events. Events triggered on the
45
+ client as `namespace.event_name` will now be dispatched the the action
46
+ subscribed to the `event_name` event under the `namespace` namespace.
47
+ See the [EventMap
48
+ Documentation](http://rdoc.info/github/DanKnox/websocket-rails/master/frames/WebsocketRails/EventMap) for more details.
49
+
44
50
  ````ruby
45
51
  # app/config/initializers
46
52
 
47
- WebsocketRails::Events.describe_events do
53
+ WebsocketRails::EventMap.describe do
48
54
  # The :client_connected method is fired automatically when a new client connects
49
55
  subscribe :client_connected, to: ChatController, with_method: :client_connected
50
56
 
@@ -55,6 +61,10 @@ WebsocketRails::Events.describe_events do
55
61
  subscribe :new_user, to: ChatController, with_method: :new_user
56
62
  subscribe :change_username, to: ChatController, with_method: :change_username
57
63
 
64
+ namespace :product do
65
+ subscribe :new, to: ProductController, with_method: :new_product
66
+ end
67
+
58
68
  # The :client_disconnected method is fired automatically when a client disconnects
59
69
  subscribe :client_disconnected, to: ChatController, with_method: :delete_user
60
70
  end
@@ -62,42 +72,32 @@ end
62
72
 
63
73
  The `subscribe` method takes the event name as the first argument, then a hash where `:to` is the Controller class and `:with_method` is the action to execute.
64
74
 
65
- ## Javascript Client
75
+ ## JavaScript Dispatcher
76
+
77
+ There are two example dispatchers located in the [assets/javascripts](https://github.com/DanKnox/websocket-rails/tree/master/assets/javascripts) directory. One connects to the server using
78
+ WebSockets and the other connects using streaming HTTP. These will eventually be merged into one dispatcher that selects the best transport at runtime based on what's available in the
79
+ browser. A pull request providing this functionality will definitely be accepted.
66
80
 
67
- The websocket client must connect to `/websocket`. You can connect using the following javascript. Replace the port with the port that your web server is running on.
81
+ The current dispatchers are limited in functionality and meant mostly as a reference implementation. The two dispatchers are functionally equivalent and can be swapped out at will.
68
82
 
69
83
  ````javascript
70
- var conn = new WebSocket("ws://localhost:3000/websocket")
71
- conn.onopen = function(evt) {
72
- // Example dispatcher located in the assets/ directory
73
- dispatcher.trigger('new_user',current_user)
74
- }
75
-
76
- conn.onmessage = function(evt) {
77
- var data = JSON.parse(evt.data),
78
- event_name = data[0],
79
- message = data[1];
80
- console.log(data)
81
- }
84
+ //Setting up the dispatcher and connecting to the server:
85
+ var dispatcher = new ServerEventsDispatcher()
86
+ dispatcher.onopen(function() {
87
+ // trigger a server event immediately after opening connection
88
+ dispatcher.trigger('new_user',{user_name: 'guest'})
89
+ })
90
+
91
+ //Triggering a new event on the server
92
+ dispatcher.trigger('event_name',object_to_be_serialized_to_json)
93
+
94
+ //Listening for new events from the server
95
+ dispatcher.bind('event_name', function(data) {
96
+ alert(data.user_name)
97
+ })
82
98
  ````
83
99
 
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.
100
+ Check out the [example application](https://github.com/DanKnox/websocket-rails-Example-Project) for a working implementation.
101
101
 
102
102
  ## Controllers
103
103
 
@@ -184,7 +184,7 @@ end
184
184
 
185
185
  ## Message Format
186
186
 
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.
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 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. The example JavaScript dispatchers decode the JSON for you as well as provide a few conveniences for event subscribing and dispatching.
188
188
 
189
189
  If you executed this code in your controller:
190
190
 
@@ -196,7 +196,7 @@ send_message :new_message, new_message
196
196
  The message that arrives on the client would look like:
197
197
 
198
198
  ````javascript
199
- ['70291412510420','new_message',{message: 'this is a message'}]
199
+ ['new_message',{message: 'this is a message'}]
200
200
  ````
201
201
 
202
202
  ## Development
@@ -21,18 +21,19 @@ var ServerEventsDispatcher = function(){
21
21
  open_handler = function(){},
22
22
  loaded = false,
23
23
  lastPos = 0,
24
- client_id = '';
24
+ client_id = 0;
25
25
 
26
26
  conn.onreadystatechange = function() {
27
27
  if (conn.readyState == 3) {
28
28
  var data = conn.responseText.substring(lastPos);
29
29
  lastPos = conn.responseText.length;
30
30
  var json_data = JSON.parse(data),
31
- id = json_data[0],
32
- event_name = json_data[1],
33
- message = json_data[2];
31
+ event_name = json_data[0],
32
+ message = json_data[1];
34
33
 
35
- client_id = id
34
+ if (client_id == 0 && event_name == 'client_connected') {
35
+ client_id = message.connection_id
36
+ }
36
37
 
37
38
  if (loaded == false) {
38
39
  open_handler();
@@ -42,11 +42,12 @@ var ServerEventsDispatcher = function(){
42
42
 
43
43
  conn.onmessage = function(evt) {
44
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
45
+ event_name = data[0],
46
+ message = data[1];
47
+
48
+ if (client_id === '' && event_name === 'client_connected') {
49
+ client_id = message.connection_id
50
+ }
50
51
  console.log(data)
51
52
  dispatch(event_name, message)
52
53
  }
@@ -62,4 +63,4 @@ var ServerEventsDispatcher = function(){
62
63
  chain[i]( message )
63
64
  }
64
65
  }
65
- }
66
+ }
@@ -20,7 +20,8 @@ end
20
20
  require "websocket_rails/engine"
21
21
  require 'websocket_rails/connection_manager'
22
22
  require 'websocket_rails/dispatcher'
23
- require 'websocket_rails/events'
23
+ require 'websocket_rails/event'
24
+ require 'websocket_rails/event_map'
24
25
  require 'websocket_rails/base_controller'
25
26
 
26
27
  require 'websocket_rails/connection_adapters'
@@ -28,4 +29,23 @@ require 'websocket_rails/connection_adapters/http'
28
29
  require 'websocket_rails/connection_adapters/web_socket'
29
30
 
30
31
  ::Thin::Server.send( :remove_const, 'DEFAULT_TIMEOUT' )
31
- ::Thin::Server.const_set( 'DEFAULT_TIMEOUT', 0 )
32
+ ::Thin::Server.const_set( 'DEFAULT_TIMEOUT', 0 )
33
+
34
+ # Exceptions
35
+ class InvalidConnectionError < StandardError
36
+ def rack_response
37
+ [400,{'Content-Type' => 'text/plain'},['invalid connection']]
38
+ end
39
+ end
40
+
41
+ # Deprecation Notices
42
+ class WebsocketRails::Dispatcher
43
+ def self.describe_events(&block)
44
+ raise "This method has been deprecated. Please use WebsocketRails::EventMap.describe instead."
45
+ end
46
+ end
47
+ class WebsocketRails::Events
48
+ def self.describe_events(&block)
49
+ raise "This method has been deprecated. Please use WebsocketRails::EventMap.describe instead."
50
+ end
51
+ end
@@ -53,7 +53,7 @@ module WebsocketRails
53
53
  # Provides direct access to the Faye::WebSocket connection object for the client that
54
54
  # initiated the event that is currently being executed.
55
55
  def connection
56
- @_connection
56
+ @_event.connection
57
57
  end
58
58
 
59
59
  # The numerical ID for the client connection that initiated the event. The ID is unique
@@ -66,23 +66,39 @@ module WebsocketRails
66
66
  # The current message that was passed from the client when the event was initiated. The
67
67
  # message is typically a standard ruby Hash object. See the README for more information.
68
68
  def message
69
- @_message
69
+ @_event.data
70
70
  end
71
+ alias_method :data, :message
71
72
 
72
73
  # Sends a message to the client that initiated the current event being executed. Messages
73
74
  # are serialized as JSON into a two element Array where the first element is the event
74
75
  # and the second element is the message that was passed, typically a Hash.
76
+ #
77
+ # # Will arrive on the client as JSON string like the following:
78
+ # # ['new_message',{'message': 'new message for the client'}]
75
79
  # message_hash = {:message => 'new message for the client'}
76
80
  # send_message :new_message, message_hash
77
- # # Will arrive on the client as JSON string like the following:
78
- # # ['new_message',{message: 'new message for the client'}]
79
- def send_message(event, message)
80
- @_dispatcher.send_message client_id, event.to_s, message, connection if @_dispatcher.respond_to?(:send_message)
81
+ #
82
+ # To send an event under a namespace, add the `:namespace => :target_namespace` option.
83
+ #
84
+ # # Will arrive as: ['product.new_message',{'message': 'new message'}]
85
+ # send_message :new_message, message_hash, :namespace => :product
86
+ #
87
+ # Nested namespaces can be passed as an array like the following:
88
+ #
89
+ # # Will arrive as: ['products.glasses.new',{'message': 'new message'}]
90
+ # send_message :new, message_hash, :namespace => [:products,:glasses]
91
+ #
92
+ # See the {EventMap} documentation for more on mapping namespaced actions.
93
+ def send_message(event_name, message, options={})
94
+ event = Event.new( event_name, message, connection, options )
95
+ @_dispatcher.send_message event if @_dispatcher.respond_to?(:send_message)
81
96
  end
82
97
 
83
98
  # Broadcasts a message to all connected clients. See {#send_message} for message passing details.
84
- def broadcast_message(event, message)
85
- @_dispatcher.broadcast_message client_id, event.to_s, message if @_dispatcher.respond_to?(:broadcast_message)
99
+ def broadcast_message(event_name, message, options={})
100
+ event = Event.new( event_name, message, connection, options )
101
+ @_dispatcher.broadcast_message event if @_dispatcher.respond_to?(:broadcast_message)
86
102
  end
87
103
 
88
104
  # Provides access to the {DataStore} for the current controller. The {DataStore} provides convenience
@@ -4,35 +4,52 @@ module WebsocketRails
4
4
  attr_reader :adapters
5
5
  module_function :adapters
6
6
 
7
- def self.register_adapter(adapter)
7
+ def self.register(adapter)
8
8
  @adapters ||= []
9
9
  @adapters.unshift adapter
10
10
  end
11
11
 
12
- def self.establish_connection(env)
13
- adapter = adapters.detect { |a| a.accepts?( env ) } || return
14
- adapter.new( env )
12
+ def self.establish_connection(env,dispatcher)
13
+ adapter = adapters.detect { |a| a.accepts?( env ) } || (raise InvalidConnectionError)
14
+ adapter.new env, dispatcher
15
15
  end
16
16
 
17
17
  class Base
18
-
19
- ADAPTER_EVENTS = [:onmessage, :onerror, :onclose]
18
+
19
+ def self.accepts?(env)
20
+ false
21
+ end
20
22
 
21
23
  def self.inherited(adapter)
22
- ConnectionAdapters.register_adapter( adapter )
24
+ ConnectionAdapters.register adapter
23
25
  end
24
26
 
25
- def initialize(env)
27
+ attr_accessor :dispatcher
28
+
29
+ def initialize(env,dispatcher)
26
30
  @env = env
31
+ @dispatcher = dispatcher
27
32
  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
33
+
34
+ def on_open(data=nil)
35
+ event = Event.new_on_open( self, data )
36
+ dispatch event
37
+ send event.serialize
38
+ end
39
+
40
+ def on_message(encoded_data)
41
+ dispatch Event.new_from_json( encoded_data, self )
42
+ end
43
+
44
+ def on_close(data=nil)
45
+ dispatch Event.new_on_close( self, data )
46
+ close_connection
47
+ end
48
+
49
+ def on_error(data=nil)
50
+ event = Event.new_on_error( self, data )
51
+ dispatch event
52
+ on_close event.data
36
53
  end
37
54
 
38
55
  def send(message)
@@ -46,6 +63,16 @@ module WebsocketRails
46
63
  def id
47
64
  object_id.to_i
48
65
  end
66
+
67
+ private
68
+
69
+ def dispatch(event)
70
+ dispatcher.dispatch( event )
71
+ end
72
+
73
+ def close_connection
74
+ dispatcher.connection_manager.close_connection self
75
+ end
49
76
  end
50
77
 
51
78
  end
@@ -3,6 +3,10 @@ module WebsocketRails
3
3
  class Http < Base
4
4
  TERM = "\r\n".freeze
5
5
  TAIL = "0#{TERM}#{TERM}".freeze
6
+ HttpHeaders = {
7
+ 'Content-Type' => 'text/json',
8
+ 'Transfer-Encoding' => 'chunked'
9
+ }
6
10
 
7
11
  def self.accepts?(env)
8
12
  true
@@ -10,15 +14,17 @@ module WebsocketRails
10
14
 
11
15
  attr_accessor :headers
12
16
 
13
- def initialize(env)
17
+ def initialize(env,dispatcher)
14
18
  super
15
19
  @body = DeferrableBody.new
16
- @headers = Hash.new
17
- @headers['Content-Type'] = 'text/json'
18
- @headers['Transfer-Encoding'] = 'chunked'
19
-
20
+ @headers = HttpHeaders
21
+
20
22
  define_deferrable_callbacks
21
- EM.next_tick { @env['async.callback'].call [200, @headers, @body] }
23
+
24
+ EM.next_tick do
25
+ @env['async.callback'].call [200, @headers, @body]
26
+ on_open
27
+ end
22
28
  end
23
29
 
24
30
  def send(message)
@@ -29,10 +35,10 @@ module WebsocketRails
29
35
 
30
36
  def define_deferrable_callbacks
31
37
  @body.callback do |event|
32
- onclose(event)
38
+ on_close(event)
33
39
  end
34
40
  @body.errback do |event|
35
- onclose(event)
41
+ on_close(event)
36
42
  end
37
43
  end
38
44
 
@@ -45,8 +51,6 @@ module WebsocketRails
45
51
  size = Rack::Utils.bytesize(c)
46
52
  return nil if size == 0
47
53
  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
54
  [size.to_s(16), TERM, c, TERM].join
51
55
  end
52
56
 
@@ -2,27 +2,28 @@ module WebsocketRails
2
2
  module ConnectionAdapters
3
3
  class WebSocket < Base
4
4
 
5
- extend Forwardable
6
-
7
5
  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
6
+ Faye::WebSocket.websocket?( env )
14
7
  end
15
- def_delegators :@connection, *delegated_methods
16
8
 
17
- def initialize(env)
9
+ def initialize(env,dispatcher)
18
10
  super
19
- @connection = ::Faye::WebSocket.new( env )
11
+ @connection = Faye::WebSocket.new( env )
12
+ @connection.onmessage = method(:on_message)
13
+ @connection.onerror = method(:on_error)
14
+ @connection.onclose = method(:on_close)
15
+ on_open
20
16
  end
21
17
 
22
18
  def send(message)
23
19
  @connection.send message
24
20
  end
21
+
22
+ def on_message(event)
23
+ data = event.respond_to?(:data) ? event.data : event
24
+ super data
25
+ end
25
26
 
26
27
  end
27
28
  end
28
- end
29
+ end