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.
- 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
|