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.
- data/CHANGELOG.md +41 -0
- data/README.md +1 -1
- data/lib/generators/websocket_rails/install/templates/events.rb +15 -5
- data/lib/rails/tasks/websocket_rails.tasks +8 -10
- data/lib/spec_helpers/matchers/route_matchers.rb +2 -1
- data/lib/spec_helpers/spec_helper_event.rb +4 -0
- data/lib/websocket-rails.rb +50 -73
- data/lib/websocket_rails/base_controller.rb +9 -7
- data/lib/websocket_rails/channel.rb +8 -1
- data/lib/websocket_rails/channel_manager.rb +6 -0
- data/lib/websocket_rails/configuration.rb +112 -0
- data/lib/websocket_rails/connection_adapters.rb +13 -4
- data/lib/websocket_rails/connection_manager.rb +3 -2
- data/lib/websocket_rails/controller_factory.rb +70 -0
- data/lib/websocket_rails/data_store.rb +128 -62
- data/lib/websocket_rails/dispatcher.rb +17 -16
- data/lib/websocket_rails/event.rb +12 -2
- data/lib/websocket_rails/event_map.rb +6 -41
- data/lib/websocket_rails/internal_events.rb +0 -7
- data/lib/websocket_rails/logging.rb +103 -14
- data/lib/websocket_rails/synchronization.rb +6 -8
- data/lib/websocket_rails/version.rb +1 -1
- data/spec/dummy/app/controllers/chat_controller.rb +6 -14
- data/spec/dummy/log/test.log +0 -750
- data/spec/integration/connection_manager_spec.rb +8 -1
- data/spec/spec_helper.rb +3 -16
- data/spec/spec_helpers/matchers/route_matchers_spec.rb +2 -11
- data/spec/spec_helpers/matchers/trigger_matchers_spec.rb +2 -12
- data/spec/unit/channel_manager_spec.rb +8 -0
- data/spec/unit/channel_spec.rb +16 -2
- data/spec/unit/connection_adapters_spec.rb +32 -11
- data/spec/unit/controller_factory_spec.rb +63 -0
- data/spec/unit/data_store_spec.rb +91 -24
- data/spec/unit/dispatcher_spec.rb +6 -11
- data/spec/unit/event_map_spec.rb +17 -27
- data/spec/unit/event_spec.rb +14 -0
- data/spec/unit/logging_spec.rb +122 -17
- 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
|
-
|
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
|
-
|
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
|
-
#
|
3
|
-
# events
|
4
|
-
#
|
5
|
-
#
|
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
|
-
#
|
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
|
-
#
|
16
|
-
#
|
17
|
-
#
|
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
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
|
25
|
-
|
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
|
-
|
44
|
-
|
45
|
-
@data[cid][k] = v
|
46
|
-
end
|
22
|
+
cattr_accessor :all_instances
|
23
|
+
@@all_instances = Hash.new { |h,k| h[k] = [] }
|
47
24
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
25
|
+
def self.clear_all_instances
|
26
|
+
@@all_instances = Hash.new { |h,k| h[k] = [] }
|
27
|
+
end
|
52
28
|
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
@
|
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 |
|
48
|
+
event_map.routes_for event do |controller_class, method|
|
56
49
|
actions << Fiber.new do
|
57
50
|
begin
|
58
|
-
|
59
|
-
|
60
|
-
|
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.
|
63
|
-
|
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 =
|
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
|
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
|
-
|
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
|
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
|