websocket-rails 0.0.1 → 0.1.0

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.
Files changed (68) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +6 -1
  5. data/Gemfile.lock +28 -12
  6. data/MIT-LICENSE +1 -1
  7. data/README.md +122 -14
  8. data/Rakefile +18 -10
  9. data/bin/thin-socketrails +30 -0
  10. data/lib/websocket-rails.rb +11 -2
  11. data/lib/websocket_rails/base_controller.rb +91 -10
  12. data/lib/websocket_rails/connection_manager.rb +57 -27
  13. data/lib/websocket_rails/data_store.rb +34 -4
  14. data/lib/websocket_rails/dispatcher.rb +25 -46
  15. data/lib/websocket_rails/events.rb +53 -0
  16. data/lib/websocket_rails/version.rb +1 -1
  17. data/{test → spec}/dummy/Rakefile +0 -0
  18. data/{test → spec}/dummy/app/controllers/application_controller.rb +0 -0
  19. data/spec/dummy/app/controllers/chat_controller.rb +57 -0
  20. data/{test → spec}/dummy/app/helpers/application_helper.rb +0 -0
  21. data/{test → spec}/dummy/app/views/layouts/application.html.erb +0 -0
  22. data/{test → spec}/dummy/config.ru +0 -0
  23. data/{test → spec}/dummy/config/application.rb +1 -1
  24. data/{test → spec}/dummy/config/boot.rb +0 -0
  25. data/{test → spec}/dummy/config/database.yml +0 -0
  26. data/{test → spec}/dummy/config/environment.rb +0 -0
  27. data/{test → spec}/dummy/config/environments/development.rb +0 -0
  28. data/{test → spec}/dummy/config/environments/production.rb +0 -0
  29. data/{test → spec}/dummy/config/environments/test.rb +0 -0
  30. data/{test → spec}/dummy/config/initializers/backtrace_silencers.rb +0 -0
  31. data/spec/dummy/config/initializers/events.rb +7 -0
  32. data/{test → spec}/dummy/config/initializers/inflections.rb +0 -0
  33. data/{test → spec}/dummy/config/initializers/mime_types.rb +0 -0
  34. data/{test → spec}/dummy/config/initializers/secret_token.rb +0 -0
  35. data/{test → spec}/dummy/config/initializers/session_store.rb +0 -0
  36. data/{test → spec}/dummy/config/locales/en.yml +0 -0
  37. data/{test → spec}/dummy/config/routes.rb +0 -0
  38. data/{test/dummy/public/favicon.ico → spec/dummy/db/test.sqlite3} +0 -0
  39. data/{test/dummy/public/stylesheets/.gitkeep → spec/dummy/log/development.log} +0 -0
  40. data/spec/dummy/log/production.log +0 -0
  41. data/spec/dummy/log/server.log +0 -0
  42. data/spec/dummy/log/test.log +0 -0
  43. data/{test → spec}/dummy/public/404.html +0 -0
  44. data/{test → spec}/dummy/public/422.html +0 -0
  45. data/{test → spec}/dummy/public/500.html +0 -0
  46. data/spec/dummy/public/favicon.ico +0 -0
  47. data/{test → spec}/dummy/public/javascripts/application.js +0 -0
  48. data/{test → spec}/dummy/public/javascripts/controls.js +0 -0
  49. data/{test → spec}/dummy/public/javascripts/dragdrop.js +0 -0
  50. data/{test → spec}/dummy/public/javascripts/effects.js +0 -0
  51. data/{test → spec}/dummy/public/javascripts/prototype.js +0 -0
  52. data/{test → spec}/dummy/public/javascripts/rails.js +0 -0
  53. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  54. data/{test → spec}/dummy/script/rails +0 -0
  55. data/spec/spec_helper.rb +60 -0
  56. data/spec/support/mock_web_socket.rb +27 -0
  57. data/spec/unit/connection_manager_spec.rb +111 -0
  58. data/spec/unit/data_store_spec.rb +15 -0
  59. data/spec/unit/dispatcher_spec.rb +57 -0
  60. data/spec/unit/events_spec.rb +70 -0
  61. data/websocket-rails.gemspec +2 -2
  62. metadata +65 -53
  63. data/lib/websocket_rails/extensions/common.rb +0 -11
  64. data/lib/websocket_rails/extensions/websocket_rack.rb +0 -55
  65. data/test/integration/navigation_test.rb +0 -7
  66. data/test/support/integration_case.rb +0 -5
  67. data/test/test_helper.rb +0 -22
  68. data/test/websocket_rails_test.rb +0 -7
@@ -1,38 +1,68 @@
1
+ require 'faye/websocket'
1
2
  require 'rack'
2
- require 'rack/websocket'
3
- require 'json'
3
+ require 'thin'
4
+
4
5
  module WebsocketRails
5
- class ConnectionManager < Rack::WebSocket::Application
6
- def initialize(*args)
7
- @dispatcher = Dispatcher.new(self)
8
- super
9
- end
10
-
11
- def on_open(env)
12
- puts "Client connected\n"
13
- @dispatcher.dispatch('client_connected',{},env)
14
- end
6
+ # The +ConnectionManager+ class implements the core Rack application that handles
7
+ # incoming WebSocket connections.
8
+ class ConnectionManager
15
9
 
16
- def on_message(env, msg)
17
- @dispatcher.receive( msg, env )
18
- end
10
+ # Contains an Array of currently open Faye::WebSocket connections.
11
+ # @return [Array]
12
+ attr_accessor :connections
19
13
 
20
- def on_error(env, error)
21
- puts "Error occured: " + error.message
14
+ def initialize
15
+ @connections = []
16
+ @dispatcher = Dispatcher.new( self )
22
17
  end
23
18
 
24
- def on_close(env)
25
- close_connection(env['websocket.client_id'])
26
- @dispatcher.dispatch('client_disconnected',{},env)
27
- puts "Client disconnected\n"
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 )
27
+
28
+ puts "Client #{connection} connected\n"
29
+ @dispatcher.dispatch( 'client_connected', {}, connection )
30
+
31
+ connection.onmessage = lambda do |event|
32
+ @dispatcher.receive( event.data, connection )
33
+ end
34
+
35
+ connection.onerror = lambda do |event|
36
+ @dispatcher.dispatch( 'client_error', {}, connection )
37
+ connection.onclose
38
+ end
39
+
40
+ connection.onclose = lambda do |event|
41
+ @dispatcher.dispatch( 'client_disconnected', {}, connection )
42
+ connections.delete( connection )
43
+
44
+ puts "Client #{connection} disconnected\n"
45
+ connection = nil
46
+ end
47
+
48
+ connections << connection
49
+ connection.rack_response
28
50
  end
29
-
30
- def send_message(msg,uid)
31
- send_data msg, uid
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
32
59
  end
33
-
34
- def broadcast_message(msg)
35
- send_data_all msg
60
+
61
+ private
62
+
63
+ def invalid_connection_attempt
64
+ [400,{'Content-Type' => 'text/plain'}, ['Connection was not a valid WebSocket connection']]
36
65
  end
66
+
37
67
  end
38
68
  end
@@ -1,5 +1,39 @@
1
1
  module WebsocketRails
2
+ # Provides a convenient way to persist data between events on a per client basis. Since every
3
+ # events from every client is executed on the same instance of the controller object, instance
4
+ # variables defined in actions will be shared between clients. The {DataStore} provides a Hash
5
+ # that is private for each connected client. It is accessed through a WebsocketRails controller
6
+ # using the {BaseController.data_store} instance method.
7
+ #
8
+ # = Example Usage
9
+ # == Creating a user
10
+ # # action on ChatController called by :client_connected event
11
+ # def new_user
12
+ # # This would be overwritten when the next user joins
13
+ # @user = User.new( message[:user_name] )
14
+ #
15
+ # # This will remain private for each user
16
+ # data_store[:user] = User.new( message[:user_name] )
17
+ # end
18
+ #
19
+ # == Collecting all Users from the DataStore
20
+ # Calling the {#each} method will yield the Hash for all connected clients:
21
+ # # From your controller
22
+ # all_users = []
23
+ # data_store.each { |store| all_users << store[:user] }
24
+ # The {DataStore} also uses method_missing to provide a convenience for the above case. Calling
25
+ # +data_store.each_<key>+ from a controller where +<key>+ is the hash key that you wish to collect
26
+ # will return an Array of the values for each connected client.
27
+ # # From your controller, assuming two users have already connected
28
+ # data_store[:user] = UserThree
29
+ # data_store.each_user
30
+ # => [UserOne,UserTwo,UserThree]
2
31
  class DataStore
32
+
33
+ extend Forwardable
34
+
35
+ def_delegator :@base, :client_id, :cid
36
+
3
37
  def initialize(base_controller)
4
38
  @base = base_controller
5
39
  @data = Hash.new {|h,k| h[k] = Hash.new}
@@ -30,10 +64,6 @@ module WebsocketRails
30
64
  @data[cid].delete(key)
31
65
  end
32
66
 
33
- def cid
34
- @base.client_id
35
- end
36
-
37
67
  def method_missing(method, *args, &block)
38
68
  if /each_(?<hash_key>\w*)/ =~ method
39
69
  results = []
@@ -1,71 +1,50 @@
1
1
  require 'json'
2
2
 
3
3
  module WebsocketRails
4
- class Dispatcher
5
- def initialize(connection)
6
- puts "Initializing dispatcher\n"
7
- @connection = connection
8
- @events = Hash.new {|h,k| h[k] = Array.new}
9
- @classes = Hash.new
10
- evaluate(&@@event_routes) if @@event_routes
4
+ class Dispatcher
5
+
6
+ def self.describe_events(&block)
7
+ raise "This method has been deprecated. Please use WebsocketRails::Events.describe_events instead."
8
+ end
9
+
10
+ attr_reader :events
11
+
12
+ def initialize(connection_manager)
13
+ @connection_manager = connection_manager
14
+ @events = Events.new( self )
11
15
  end
12
16
 
13
- def receive(enc_message,env)
17
+ def receive(enc_message,connection)
14
18
  message = JSON.parse( enc_message )
15
19
  event_name = message.first
16
20
  data = message.last
17
21
  data['received'] = Time.now.strftime("%I:%M:%p")
18
- dispatch( event_name, data, env )
22
+ dispatch( event_name, data, connection )
19
23
  end
20
24
 
21
- def send_message(event_name,data)
22
- @connection.send_message encoded_message( event_name, data.last ), data.first
25
+ def send_message(event_name,data,connection)
26
+ connection.send encoded_message( event_name, data )
23
27
  end
24
28
 
25
29
  def broadcast_message(event_name,data)
26
- @connection.broadcast_message encoded_message( event_name, data )
30
+ @connection_manager.broadcast_message encoded_message( event_name, data )
27
31
  end
28
32
 
29
- def dispatch(event_name,data,env)
30
- puts "#{event_name} is handled by #{@events[event_name.to_sym].inspect}\n\n"
31
- message = [env['websocket.client_id'],data]
33
+ def dispatch(event_name,message,connection)
32
34
  Fiber.new {
33
- @events[event_name.to_sym].each do |event|
34
- handler = event.first
35
- klass = @classes[handler]
36
- klass.instance_variable_set(:@_message,message)
37
- method = event.last
38
- klass.send( method )
35
+ event_symbol = event_name.to_sym
36
+ events.routes_for(event_symbol) do |controller,method|
37
+ controller.instance_variable_set(:@_message,message)
38
+ controller.instance_variable_set(:@_connection,connection)
39
+ controller.send :execute_observers, event_symbol
40
+ controller.send method if controller.respond_to?(method)
39
41
  end
40
42
  }.resume
41
43
  end
42
-
43
- def close_connection
44
- @connection.close_connection
45
- end
46
-
44
+
47
45
  def encoded_message(event_name,data)
48
46
  [event_name, data].to_json
49
47
  end
50
-
51
- def subscribe(event_name,options)
52
- klass = options[:to] || raise("Must specify a class for to: option in event route")
53
- method = options[:with_method] || raise("Must specify a method for with_method: option in event route")
54
- controller = klass.new
55
- if @classes[klass].nil?
56
- @classes[klass] = controller
57
- controller.instance_variable_set(:@_dispatcher,self)
58
- controller.send :initialize_session if controller.respond_to?(:initialize_session)
59
- end
60
- @events[event_name] << [klass,method]
61
- end
62
-
63
- def self.describe_events(&block)
64
- @@event_routes = block
65
- end
66
-
67
- def evaluate(&block)
68
- instance_eval &block
69
- end
48
+
70
49
  end
71
50
  end
@@ -0,0 +1,53 @@
1
+ module WebsocketRails
2
+ # Provides a DSL for mapping client events to controller actions. A single event can be mapped to any
3
+ # number of controllers and actions. You can define your event routes by creating an +events.rb+ file in
4
+ # your application's +initializers+ directory. The DSL currently consists of a single method, {#subscribe},
5
+ # which takes a symbolized event name as the first argument, and a Hash with the controller and method
6
+ # name as the second arguments.
7
+ #
8
+ # == Example events.rb file
9
+ # # located in config/initializers/events.rb
10
+ # WebsocketRails::Events.describe_events do
11
+ # subscribe :client_connected, to: ChatController, with_method: :client_connected
12
+ # subscribe :new_user, to: ChatController, with_method: :new_user
13
+ # end
14
+ class Events
15
+
16
+ def self.describe_events(&block)
17
+ WebsocketRails.route_block = block
18
+ end
19
+
20
+ attr_reader :classes, :events
21
+
22
+ def initialize(dispatcher)
23
+ @dispatcher = dispatcher
24
+ evaluate( WebsocketRails.route_block ) if WebsocketRails.route_block
25
+ end
26
+
27
+ def routes_for(event,&block)
28
+ @events[event].each do |klass,method|
29
+ controller = @classes[klass]
30
+ block.call( controller, method )
31
+ end
32
+ end
33
+
34
+ def subscribe(event_name,options)
35
+ klass = options[:to] || raise("Must specify a class for to: option in event route")
36
+ method = options[:with_method] || raise("Must specify a method for with_method: option in event route")
37
+ controller = klass.new
38
+ if @classes[klass].nil?
39
+ @classes[klass] = controller
40
+ controller.instance_variable_set(:@_dispatcher,@dispatcher)
41
+ controller.send :initialize_session if controller.respond_to?(:initialize_session)
42
+ end
43
+ @events[event_name] << [klass,method]
44
+ end
45
+
46
+ def evaluate(block)
47
+ @events = Hash.new {|h,k| h[k] = Array.new}
48
+ @classes = Hash.new
49
+ instance_eval &block
50
+ end
51
+
52
+ end
53
+ end
@@ -1,3 +1,3 @@
1
1
  module WebsocketRails
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
File without changes
@@ -0,0 +1,57 @@
1
+ class ChatController < WebsocketRails::BaseController
2
+
3
+ observe {
4
+ if data_store.each_user.count > 0
5
+ puts 'it worked'
6
+ end
7
+
8
+ if message_counter > 10
9
+ puts 'message counter needs to be dumped'
10
+ self.message_counter = 0
11
+ end
12
+ }
13
+
14
+ observe(:new_message) {
15
+ puts "message observer fired for #{message}"
16
+ }
17
+
18
+ attr_accessor :message_counter
19
+
20
+ def initialize_session
21
+ # perform application setup here
22
+ @message_counter = 0
23
+ end
24
+
25
+ def client_connected
26
+ # do something when a client connects
27
+ end
28
+
29
+ def new_message
30
+ puts "Message from UID: #{client_id}\n"
31
+ @message_counter += 1
32
+ broadcast_message :new_message, message
33
+ end
34
+
35
+ def new_user
36
+ puts "storing user in data store\n"
37
+ data_store[:user] = message
38
+ broadcast_user_list
39
+ end
40
+
41
+ def change_username
42
+ data_store[:user] = message
43
+ broadcast_user_list
44
+ end
45
+
46
+ def delete_user
47
+ data_store.remove_client
48
+ broadcast_user_list
49
+ end
50
+
51
+ def broadcast_user_list
52
+ users = data_store.each_user
53
+ puts "broadcasting user list: #{users}\n"
54
+ broadcast_message :user_list, users
55
+ end
56
+
57
+ end
File without changes
@@ -7,7 +7,7 @@ require "action_view/railtie"
7
7
  require "action_mailer/railtie"
8
8
 
9
9
  Bundler.require
10
- require "websocket_rails"
10
+ require "websocket-rails"
11
11
 
12
12
  module Dummy
13
13
  class Application < Rails::Application
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ WebsocketRails::Events.describe_events do
2
+ subscribe :client_connected, to: ChatController, with_method: :client_connected
3
+ subscribe :new_message, to: ChatController, with_method: :new_message
4
+ subscribe :new_user, to: ChatController, with_method: :new_user
5
+ subscribe :change_username, to: ChatController, with_method: :change_username
6
+ subscribe :client_disconnected, to: ChatController, with_method: :delete_user
7
+ end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes