websocket-rails 0.3.0 → 0.4.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 (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