websocket-rails 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/CHANGELOG.md +41 -0
  2. data/README.md +1 -1
  3. data/lib/generators/websocket_rails/install/templates/events.rb +15 -5
  4. data/lib/rails/tasks/websocket_rails.tasks +8 -10
  5. data/lib/spec_helpers/matchers/route_matchers.rb +2 -1
  6. data/lib/spec_helpers/spec_helper_event.rb +4 -0
  7. data/lib/websocket-rails.rb +50 -73
  8. data/lib/websocket_rails/base_controller.rb +9 -7
  9. data/lib/websocket_rails/channel.rb +8 -1
  10. data/lib/websocket_rails/channel_manager.rb +6 -0
  11. data/lib/websocket_rails/configuration.rb +112 -0
  12. data/lib/websocket_rails/connection_adapters.rb +13 -4
  13. data/lib/websocket_rails/connection_manager.rb +3 -2
  14. data/lib/websocket_rails/controller_factory.rb +70 -0
  15. data/lib/websocket_rails/data_store.rb +128 -62
  16. data/lib/websocket_rails/dispatcher.rb +17 -16
  17. data/lib/websocket_rails/event.rb +12 -2
  18. data/lib/websocket_rails/event_map.rb +6 -41
  19. data/lib/websocket_rails/internal_events.rb +0 -7
  20. data/lib/websocket_rails/logging.rb +103 -14
  21. data/lib/websocket_rails/synchronization.rb +6 -8
  22. data/lib/websocket_rails/version.rb +1 -1
  23. data/spec/dummy/app/controllers/chat_controller.rb +6 -14
  24. data/spec/dummy/log/test.log +0 -750
  25. data/spec/integration/connection_manager_spec.rb +8 -1
  26. data/spec/spec_helper.rb +3 -16
  27. data/spec/spec_helpers/matchers/route_matchers_spec.rb +2 -11
  28. data/spec/spec_helpers/matchers/trigger_matchers_spec.rb +2 -12
  29. data/spec/unit/channel_manager_spec.rb +8 -0
  30. data/spec/unit/channel_spec.rb +16 -2
  31. data/spec/unit/connection_adapters_spec.rb +32 -11
  32. data/spec/unit/controller_factory_spec.rb +63 -0
  33. data/spec/unit/data_store_spec.rb +91 -24
  34. data/spec/unit/dispatcher_spec.rb +6 -11
  35. data/spec/unit/event_map_spec.rb +17 -27
  36. data/spec/unit/event_spec.rb +14 -0
  37. data/spec/unit/logging_spec.rb +122 -17
  38. metadata +11 -6
@@ -26,14 +26,15 @@ module WebsocketRails
26
26
  ConnectionAdapters.register adapter
27
27
  end
28
28
 
29
- attr_reader :dispatcher, :queue, :env, :request
29
+ attr_reader :dispatcher, :queue, :env, :request, :data_store
30
30
 
31
- def initialize(request,dispatcher)
31
+ def initialize(request, dispatcher)
32
32
  @env = request.env.dup
33
33
  @request = request
34
- @queue = EventQueue.new
35
34
  @dispatcher = dispatcher
36
35
  @connected = true
36
+ @queue = EventQueue.new
37
+ @data_store = DataStore::Connection.new(self)
37
38
  @delegate = WebsocketRails::DelegationController.new
38
39
  @delegate.instance_variable_set(:@_env,request.env)
39
40
  @delegate.instance_variable_set(:@_request,request)
@@ -107,6 +108,14 @@ module WebsocketRails
107
108
  @delegate
108
109
  end
109
110
 
111
+ def inspect
112
+ "#<Connnection::#{id}>"
113
+ end
114
+
115
+ def to_s
116
+ inspect
117
+ end
118
+
110
119
  private
111
120
 
112
121
  def dispatch(event)
@@ -114,6 +123,7 @@ module WebsocketRails
114
123
  end
115
124
 
116
125
  def close_connection
126
+ @data_store.destroy!
117
127
  dispatcher.connection_manager.close_connection self
118
128
  end
119
129
 
@@ -123,7 +133,6 @@ module WebsocketRails
123
133
  def start_ping_timer
124
134
  @pong = true
125
135
  @ping_timer = EM::PeriodicTimer.new(10) do
126
- log "ping"
127
136
  if pong == true
128
137
  self.pong = false
129
138
  ping = Event.new_on_ping self
@@ -71,13 +71,14 @@ module WebsocketRails
71
71
  def open_connection(request)
72
72
  connection = ConnectionAdapters.establish_connection( request, dispatcher )
73
73
  connections << connection
74
- log "Connection opened: #{connection}"
74
+ info "Connection opened: #{connection}"
75
75
  connection.rack_response
76
76
  end
77
77
 
78
78
  def close_connection(connection)
79
+ WebsocketRails.channel_manager.unsubscribe connection
79
80
  connections.delete connection
80
- log "Connection closed: #{connection}"
81
+ info "Connection closed: #{connection}"
81
82
  connection = nil
82
83
  end
83
84
  public :close_connection
@@ -0,0 +1,70 @@
1
+ module WebsocketRails
2
+ class ControllerFactory
3
+
4
+ attr_reader :controller_stores, :dispatcher
5
+
6
+ def initialize(dispatcher)
7
+ @dispatcher = dispatcher
8
+ @controller_stores = {}
9
+ @initialized_controllers = {}
10
+ end
11
+
12
+ # TODO: Add deprecation notice for user defined
13
+ # instance variables.
14
+ def new_for_event(event, controller_class)
15
+ reload! controller_class
16
+ controller = controller_class.new
17
+
18
+ prepare(controller, event)
19
+
20
+ controller
21
+ end
22
+
23
+ private
24
+
25
+ def store_for_controller(controller)
26
+ @controller_stores[controller.class] ||= DataStore::Controller.new(controller)
27
+ end
28
+
29
+ def prepare(controller, event)
30
+ set_event(controller, event)
31
+ set_dispatcher(controller, dispatcher)
32
+ set_controller_store(controller)
33
+ initialize_controller(controller)
34
+ end
35
+
36
+ def set_event(controller, event)
37
+ set_ivar :@_event, controller, event
38
+ end
39
+
40
+ def set_dispatcher(controller, dispatcher)
41
+ set_ivar :@_dispatcher, controller, dispatcher
42
+ end
43
+
44
+ def set_controller_store(controller)
45
+ set_ivar :@_controller_store, controller, store_for_controller(controller)
46
+ end
47
+
48
+ def set_ivar(ivar, object, value)
49
+ object.instance_variable_set(ivar, value)
50
+ end
51
+
52
+ def initialize_controller(controller)
53
+ unless @initialized_controllers[controller.class] == true
54
+ controller.send(:initialize_session) if controller.respond_to?(:initialize_session)
55
+ @initialized_controllers[controller.class] = true
56
+ end
57
+ end
58
+
59
+ # Reloads the controller class to pick up code changes
60
+ # while in the development environment.
61
+ def reload!(controller)
62
+ return unless defined?(Rails) and Rails.env.development?
63
+
64
+ class_name = controller.name
65
+ filename = class_name.underscore
66
+ load "#{filename}.rb"
67
+ end
68
+
69
+ end
70
+ end
@@ -1,79 +1,145 @@
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.
2
+ # The {DataStore} provides a convenient way to persist information between
3
+ # execution of events. Since every event is executed within a new instance
4
+ # of the controller class, instance variables set while processing an
5
+ # action will be lost after the action finishes executing.
7
6
  #
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] )
7
+ # There are two different {DataStore} classes that you can use:
14
8
  #
15
- # # This will remain private for each user
16
- # data_store[:user] = User.new( message[:user_name] )
17
- # end
9
+ # The {DataStore::Connection} class is unique for every active connection.
10
+ # You can use it similar to the Rails session store. The connection data
11
+ # store can be accessed within your controller using the `#connection_store`
12
+ # method.
18
13
  #
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]
31
- class DataStore
32
-
33
- extend Forwardable
34
-
35
- def_delegator :@base, :client_id, :cid
36
-
37
- def initialize(base_controller)
38
- @base = base_controller
39
- @data = Hash.new {|h,k| h[k] = Hash.new}
40
- @data = @data.with_indifferent_access
41
- end
14
+ # The {DataStore::Controller} class is unique for every controller. You
15
+ # can use it similar to how you would use instance variables within a
16
+ # plain ruby class. The values set within the controller store will be
17
+ # persisted between events. The controller store can be accessed within
18
+ # your controller using the `#controller_store` method.
19
+ module DataStore
20
+ class Base < ActiveSupport::HashWithIndifferentAccess
42
21
 
43
- def []=(k,v)
44
- @data[cid] = Hash.new unless @data[cid]
45
- @data[cid][k] = v
46
- end
22
+ cattr_accessor :all_instances
23
+ @@all_instances = Hash.new { |h,k| h[k] = [] }
47
24
 
48
- def [](k)
49
- @data[cid] = Hash.new unless @data[cid]
50
- @data[cid][k]
51
- end
25
+ def self.clear_all_instances
26
+ @@all_instances = Hash.new { |h,k| h[k] = [] }
27
+ end
52
28
 
53
- def each(&block)
54
- @data.each do |cid,hash|
55
- block.call(hash) if block
29
+ def initialize
30
+ instances << self
31
+ end
32
+
33
+ def instances
34
+ all_instances[self.class]
35
+ end
36
+
37
+ def collect_all(key)
38
+ collection = instances.each_with_object([]) do |instance, array|
39
+ array << instance[key]
40
+ end
41
+
42
+ if block_given?
43
+ collection.each do |item|
44
+ yield(item)
45
+ end
46
+ else
47
+ collection
48
+ end
49
+ end
50
+
51
+ def destroy!
52
+ instances.delete_if {|store| store.object_id == self.object_id }
56
53
  end
57
- end
58
54
 
59
- def remove_client
60
- @data.delete(cid)
61
55
  end
62
56
 
63
- def delete(key)
64
- @data[cid].delete(key)
57
+ # The connection data store operates much like the {Controller} store. The
58
+ # biggest difference is that the data placed inside is private for
59
+ # individual users and accessible from any controller. Anything placed
60
+ # inside the connection data store will be deleted when a user disconnects.
61
+ #
62
+ # The connection data store is accessed through the `#connection_store`
63
+ # instance method inside your controller.
64
+ #
65
+ # If we were writing a basic chat system, we could use the connection data
66
+ # store to hold onto a user's current screen name.
67
+ #
68
+ #
69
+ # class UserController < WebsocketRails::BaseController
70
+ #
71
+ # def set_screen_name
72
+ # connection_store[:screen_name] = message[:screen_name]
73
+ # end
74
+ #
75
+ # end
76
+ #
77
+ # class ChatController < WebsocketRails::BaseController
78
+ #
79
+ # def say_hello
80
+ # screen_name = connection_store[:screen_name]
81
+ # send_message :new_message, "#{screen_name} says hello"
82
+ # end
83
+ #
84
+ # end
85
+ class Connection < Base
86
+
87
+ attr_accessor :connection
88
+
89
+ def initialize(connection)
90
+ super()
91
+ @connection = connection
92
+ end
93
+
65
94
  end
66
95
 
67
- def method_missing(method, *args, &block)
68
- if /each_(?<hash_key>\w*)/ =~ method
69
- results = []
70
- @data.each do |cid,hash|
71
- results << hash[hash_key]
72
- end
73
- results
74
- else
75
- super
96
+ # The Controller DataStore acts as a stand-in for instance variables in your
97
+ # controller. At it's core, it is a Hash which is accessible inside your
98
+ # controller through the `#controller_store` instance method. Any values
99
+ # set in the controller store will be visible by all connected users which
100
+ # trigger events that use that controller. However, values set in one
101
+ # controller will not be visible by other controllers.
102
+ #
103
+ #
104
+ # class AccountController < WebsocketRails::BaseController
105
+ # # We will use an Event Observer to set the initial value
106
+ # observe { controller_store[:event_count] ||= 0 }
107
+ #
108
+ # # Mapped as `accounts.important_event` in the Event Router
109
+ # def important_event
110
+ # # This will be private for each controller
111
+ # controller_store[:event_count] += 1
112
+ # trigger_success controller_store[:event_count]
113
+ # end
114
+ # end
115
+ #
116
+ # class ProductController < WebsocketRails::BaseController
117
+ # # We will use an Event Observer to set the initial value
118
+ # observe { controller_store[:event_count] ||= 0 }
119
+ #
120
+ # # Mapped as `products.boring_event` in the Event Router
121
+ # def boring_event
122
+ # # This will be private for each controller
123
+ # controller_store[:event_count] += 1
124
+ # trigger_success controller_store[:event_count]
125
+ # end
126
+ # end
127
+ #
128
+ # # trigger `accounts.important_event`
129
+ # => 1
130
+ # # trigger `accounts.important_event`
131
+ # => 2
132
+ # # trigger `products.boring_event`
133
+ # => 1
134
+ class Controller < Base
135
+
136
+ attr_accessor :controller
137
+
138
+ def initialize(controller)
139
+ super()
140
+ @controller = controller
76
141
  end
142
+
77
143
  end
78
144
  end
79
145
  end
@@ -1,15 +1,14 @@
1
- #require 'actionpack/action_dispatch/request'
2
-
3
1
  module WebsocketRails
4
2
  class Dispatcher
5
3
 
6
4
  include Logging
7
5
 
8
- attr_reader :event_map, :connection_manager
6
+ attr_reader :event_map, :connection_manager, :controller_factory
9
7
 
10
8
  def initialize(connection_manager)
11
9
  @connection_manager = connection_manager
12
- @event_map = EventMap.new( self )
10
+ @controller_factory = ControllerFactory.new(self)
11
+ @event_map = EventMap.new(self)
13
12
  end
14
13
 
15
14
  def receive_encoded(encoded_data,connection)
@@ -25,8 +24,6 @@ module WebsocketRails
25
24
  def dispatch(event)
26
25
  return if event.is_invalid?
27
26
 
28
- log "Event received: #{event.name}"
29
-
30
27
  if event.is_channel?
31
28
  WebsocketRails[event.channel].trigger_event event
32
29
  else
@@ -44,23 +41,27 @@ module WebsocketRails
44
41
  end
45
42
  end
46
43
 
47
- def reload_controllers!
48
- @event_map.reload_controllers!
49
- end
50
-
51
44
  private
52
45
 
53
46
  def route(event)
54
47
  actions = []
55
- event_map.routes_for event do |controller, method|
48
+ event_map.routes_for event do |controller_class, method|
56
49
  actions << Fiber.new do
57
50
  begin
58
- controller.instance_variable_set(:@_event,event)
59
- controller.send(:execute_observers, event.name) if controller.respond_to?(:execute_observers)
60
- result = controller.send(method) if controller.respond_to?(method)
51
+ log_event(event) do
52
+ controller = controller_factory.new_for_event(event, controller_class)
53
+
54
+ if controller.respond_to?(:execute_observers)
55
+ controller.send(:execute_observers, event.name)
56
+ end
57
+
58
+ if controller.respond_to?(method)
59
+ controller.send(method)
60
+ else
61
+ raise EventRoutingError.new(event, controller, method)
62
+ end
63
+ end
61
64
  rescue Exception => ex
62
- puts ex.backtrace
63
- puts "Application Exception: #{ex}"
64
65
  event.success = false
65
66
  event.data = extract_exception_data ex
66
67
  event.trigger
@@ -58,14 +58,20 @@ module WebsocketRails
58
58
  # :channel =>
59
59
  # The name of the channel that this event is destined for.
60
60
  class Event
61
+ extend Logging
61
62
 
62
- def self.new_from_json(encoded_data,connection)
63
- log "Event Data: #{encoded_data}"
63
+ def self.log_header
64
+ "Event"
65
+ end
66
+
67
+ def self.new_from_json(encoded_data, connection)
64
68
  event_name, data = JSON.parse encoded_data
65
69
  data = data.merge(:connection => connection).with_indifferent_access
66
70
  Event.new event_name, data
67
71
  rescue JSON::ParserError => ex
68
72
  warn "Invalid Event Received: #{ex}"
73
+ debug "Event Data: #{encoded_data}"
74
+ log_exception(ex)
69
75
  Event.new_on_invalid_event_received(connection, nil)
70
76
  end
71
77
 
@@ -115,6 +121,10 @@ module WebsocketRails
115
121
  name == :invalid_event
116
122
  end
117
123
 
124
+ def is_internal?
125
+ namespace.include?(:websocket_rails)
126
+ end
127
+
118
128
  def trigger
119
129
  connection.trigger self if connection
120
130
  end
@@ -20,14 +20,14 @@ module WebsocketRails
20
20
  class EventMap
21
21
 
22
22
  def self.describe(&block)
23
- WebsocketRails.route_block = block
23
+ WebsocketRails.config.route_block = block
24
24
  end
25
25
 
26
26
  attr_reader :namespace
27
27
 
28
28
  def initialize(dispatcher)
29
29
  @dispatcher = dispatcher
30
- @namespace = DSL.new(dispatcher).evaluate WebsocketRails.route_block
30
+ @namespace = DSL.new(dispatcher).evaluate WebsocketRails.config.route_block
31
31
  @namespace = DSL.new(dispatcher,@namespace).evaluate InternalEvents.events
32
32
  end
33
33
 
@@ -99,40 +99,13 @@ module WebsocketRails
99
99
  # Stores controller/action pairs for events subscribed under
100
100
  # this namespace.
101
101
  def store(event_name,options)
102
- klass, action = TargetValidator.validate_target options
103
- create_controller_instance_for klass if controllers[klass].nil?
102
+ klass, action = TargetValidator.validate_target options
104
103
  actions[event_name] << [klass,action]
105
104
  end
106
105
 
107
-
108
-
109
- # Reloads the controller instances stored in the event map
110
- # collection, picking up code changes in development.
111
- def reload_controllers!
112
- return unless defined?(Rails) and
113
- Rails.env.development? or Rails.env.test?
114
-
115
- controllers.each_key do |klass|
116
- data_store = controllers[klass].data_store
117
- class_name = klass.name
118
- filename = class_name.underscore
119
- load "#{filename}.rb"
120
- new_class = class_name.safe_constantize
121
-
122
- controller = new_class.new
123
- controller.instance_variable_set(:@_dispatcher,@dispatcher)
124
- controller.instance_variable_set(:@data_store,data_store)
125
- controller.send :initialize_session if controller.respond_to?(:initialize_session)
126
- controllers[klass] = controller
127
- end
128
- unless namespaces.empty?
129
- namespaces.each_value { |ns| ns.reload_controllers! unless ns.name == :websocket_rails }
130
- end
131
- end
132
-
133
106
  # Iterates through the namespace tree and yields all
134
107
  # controller/action pairs stored for the target event.
135
- def routes_for(event,event_namespace=nil,&block)
108
+ def routes_for(event, event_namespace=nil, &block)
136
109
 
137
110
  # Grab the first level namespace from the namespace array
138
111
  # and remove it from the copy.
@@ -149,8 +122,7 @@ module WebsocketRails
149
122
  # copy of the event's namespace array.
150
123
  if namespace == @name and event_namespace.empty?
151
124
  actions[event.name].each do |klass,action|
152
- controller = controllers[klass]
153
- block.call controller, action
125
+ block.call klass, action
154
126
  end
155
127
  else
156
128
  child_namespace = event_namespace.first
@@ -161,14 +133,7 @@ module WebsocketRails
161
133
 
162
134
  private
163
135
 
164
- def create_controller_instance_for(klass)
165
- controller = klass.new
166
- controllers[klass] = controller
167
- controller.instance_variable_set(:@_dispatcher,@dispatcher)
168
- controller.send :initialize_session if controller.respond_to?(:initialize_session)
169
- end
170
-
171
- def copy_event_namespace(event,namespace=nil)
136
+ def copy_event_namespace(event, namespace=nil)
172
137
  namespace = event.namespace.dup if namespace.nil?
173
138
  namespace
174
139
  end