websocket-rails 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,14 @@
1
1
  # WebsocketRails Change Log
2
2
 
3
+ ## Version 0.6.0
4
+
5
+ September 3 2013
6
+
7
+ * Added the UserManager accessible through the `WebsocketRails.users`
8
+ method. This allows for triggering events on individual logged in users
9
+ from anywhere inside of your application without the need to create a
10
+ channel for that user.
11
+
3
12
  ## Version 0.5.0
4
13
 
5
14
  September 2 2013
@@ -8,13 +17,13 @@ September 2 2013
8
17
  shcheme. - Thanks to @depili
9
18
  * Override ConnectionManager#inspect to clean up the output from `rake
10
19
  routes`
11
- * Add a basic Global UserManager for triggering events on specific users
20
+ * Added a basic Global UserManager for triggering events on specific users
12
21
  from anywhere inside your app without creating a dedicated user channel.
13
22
  * Deprecate the old controller observer system and implement full Rails
14
23
  AbstractController::Callbacks support. - Thanks to @pitr
15
24
  * Reload the events.rb event route file each time an event is fired. -
16
25
  Thanks to @moaa
17
- * Separate the event route file and WebsocketRails configuration files.
26
+ * Separated the event route file and WebsocketRails configuration files.
18
27
  The events.rb now lives in `config/events.rb`. The configuration should
19
28
  remain in an initializer located at `config/initializers/websocket_rails.rb`. - Thanks to @moaa
20
29
 
@@ -38,4 +38,11 @@ WebsocketRails.setup do |config|
38
38
  # if one exists. If `current_user` does not exist or does not
39
39
  # respond to the identifier, the key will default to `connection.id`
40
40
  # config.user_identifier = :id
41
+
42
+ # Uncomment and change this option to override the class associated
43
+ # with your `current_user` object. This class will be used when
44
+ # synchronization is enabled and you trigger events from background
45
+ # jobs using the WebsocketRails.users UserManager.
46
+ # config.user_class = User
47
+
41
48
  end
@@ -9,6 +9,14 @@ module WebsocketRails
9
9
  @user_identifier = identifier
10
10
  end
11
11
 
12
+ def user_class
13
+ @user_class ||= User
14
+ end
15
+
16
+ def user_class=(klass)
17
+ @user_class = klass
18
+ end
19
+
12
20
  def keep_subscribers_when_private?
13
21
  @keep_subscribers_when_private ||= false
14
22
  end
@@ -10,7 +10,7 @@ module WebsocketRails
10
10
  end
11
11
 
12
12
  def self.establish_connection(request, dispatcher)
13
- adapter = adapters.detect { |a| a.accepts?( request.env ) } || (raise InvalidConnectionError)
13
+ adapter = adapters.detect { |a| a.accepts?(request.env) } || raise(InvalidConnectionError)
14
14
  adapter.new request, dispatcher
15
15
  end
16
16
 
@@ -28,6 +28,10 @@ module WebsocketRails
28
28
 
29
29
  attr_reader :dispatcher, :queue, :env, :request, :data_store
30
30
 
31
+ # The ConnectionManager will set the connection ID when the
32
+ # connection is opened.
33
+ attr_accessor :id
34
+
31
35
  def initialize(request, dispatcher)
32
36
  @env = request.env.dup
33
37
  @request = request
@@ -39,7 +43,6 @@ module WebsocketRails
39
43
  @delegate.instance_variable_set(:@_env, request.env)
40
44
  @delegate.instance_variable_set(:@_request, request)
41
45
 
42
- WebsocketRails.users[user_identifier] = self
43
46
  start_ping_timer
44
47
  end
45
48
 
@@ -70,28 +73,16 @@ module WebsocketRails
70
73
  @queue << event
71
74
  end
72
75
 
73
- attr_accessor :flush_scheduled
74
-
75
76
  def trigger(event)
76
- # Uncomment when implementing history queueing with redis
77
- #enqueue event
78
- #unless flush_scheduled
79
- # EM.next_tick { flush; flush_scheduled = false }
80
- # flush_scheduled = true
81
- #end
82
77
  send "[#{event.serialize}]"
83
78
  end
84
79
 
85
80
  def flush
86
- count = 1
87
- message = "["
81
+ message = []
88
82
  @queue.flush do |event|
89
- message << event.serialize
90
- message << "," unless count == @queue.size
91
- count += 1
83
+ message << event.as_json
92
84
  end
93
- message << "]"
94
- send message
85
+ send message.to_json
95
86
  end
96
87
 
97
88
  def send_message(event_name, data = {}, options = {})
@@ -110,14 +101,14 @@ module WebsocketRails
110
101
  [ -1, {}, [] ]
111
102
  end
112
103
 
113
- def id
114
- object_id.to_i
115
- end
116
-
117
104
  def controller_delegate
118
105
  @delegate
119
106
  end
120
107
 
108
+ def connected?
109
+ true & @connected
110
+ end
111
+
121
112
  def inspect
122
113
  "#<Connection::#{id}>"
123
114
  end
@@ -126,31 +117,46 @@ module WebsocketRails
126
117
  inspect
127
118
  end
128
119
 
129
- private
120
+ def user_connection?
121
+ not user_identifier.nil?
122
+ end
123
+
124
+ def user
125
+ return unless user_connection?
126
+ controller_delegate.current_user
127
+ end
130
128
 
131
129
  def user_identifier
132
130
  @user_identifier ||= begin
133
131
  identifier = WebsocketRails.config.user_identifier
134
132
 
135
- unless @delegate.respond_to?(:current_user) &&
136
- @delegate.current_user &&
137
- @delegate.current_user.respond_to?(identifier)
138
- return id
139
- end
133
+ return unless current_user_responds_to?(identifier)
140
134
 
141
135
  controller_delegate.current_user.send(identifier)
142
136
  end
143
137
  end
144
138
 
139
+ private
140
+
145
141
  def dispatch(event)
146
142
  dispatcher.dispatch event
147
143
  end
148
144
 
145
+ def connection_manager
146
+ dispatcher.connection_manager
147
+ end
148
+
149
149
  def close_connection
150
150
  @data_store.destroy!
151
151
  @ping_timer.try(:cancel)
152
152
  dispatcher.connection_manager.close_connection self
153
- WebsocketRails.users.delete(user_identifier)
153
+ end
154
+
155
+ def current_user_responds_to?(identifier)
156
+ controller_delegate &&
157
+ controller_delegate.respond_to?(:current_user) &&
158
+ controller_delegate.current_user &&
159
+ controller_delegate.current_user.respond_to?(identifier)
154
160
  end
155
161
 
156
162
  attr_accessor :pong
@@ -171,6 +177,5 @@ module WebsocketRails
171
177
  end
172
178
 
173
179
  end
174
-
175
180
  end
176
181
  end
@@ -15,8 +15,8 @@ module WebsocketRails
15
15
  BadRequestResponse = [400,{'Content-Type' => 'text/plain'},['invalid']].freeze
16
16
  ExceptionResponse = [500,{'Content-Type' => 'text/plain'},['exception']].freeze
17
17
 
18
- # Contains an Array of currently open connections.
19
- # @return [Array]
18
+ # Contains a Hash of currently open connections.
19
+ # @return [Hash]
20
20
  attr_reader :connections
21
21
 
22
22
  # Contains the {Dispatcher} instance for the active server.
@@ -28,7 +28,7 @@ module WebsocketRails
28
28
  attr_reader :synchronization
29
29
 
30
30
  def initialize
31
- @connections = []
31
+ @connections = {}
32
32
  @dispatcher = Dispatcher.new(self)
33
33
 
34
34
  if WebsocketRails.synchronize?
@@ -63,31 +63,57 @@ module WebsocketRails
63
63
  private
64
64
 
65
65
  def parse_incoming_event(params)
66
- connection = find_connection_by_id params["client_id"]
66
+ connection = find_connection_by_id(params["client_id"].to_i)
67
67
  connection.on_message params["data"]
68
68
  SuccessfulResponse
69
69
  end
70
70
 
71
71
  def find_connection_by_id(id)
72
- connections.detect { |connection| connection.id == id.to_i } || (raise InvalidConnectionError)
72
+ connections[id] || raise(InvalidConnectionError)
73
73
  end
74
74
 
75
75
  # Opens a persistent connection using the appropriate {ConnectionAdapter}. Stores
76
- # active connections in the {connections} array.
76
+ # active connections in the {connections} Hash.
77
77
  def open_connection(request)
78
- connection = ConnectionAdapters.establish_connection( request, dispatcher )
79
- connections << connection
78
+ connection = ConnectionAdapters.establish_connection(request, dispatcher)
79
+
80
+ assign_connection_id connection
81
+ register_user_connection connection
82
+
83
+ connections[connection.id] = connection
84
+
80
85
  info "Connection opened: #{connection}"
81
86
  connection.rack_response
82
87
  end
83
88
 
84
89
  def close_connection(connection)
85
90
  WebsocketRails.channel_manager.unsubscribe connection
86
- connections.delete connection
91
+ destroy_user_connection connection
92
+
93
+ connections.delete connection.id
94
+
87
95
  info "Connection closed: #{connection}"
88
96
  connection = nil
89
97
  end
90
98
  public :close_connection
91
99
 
100
+ def assign_connection_id(connection)
101
+ begin
102
+ id = SecureRandom.hex(10)
103
+ end while connections.has_key?(id)
104
+
105
+ connection.id = id
106
+ end
107
+
108
+ def register_user_connection(connection)
109
+ return unless connection.user_connection?
110
+ WebsocketRails.users[connection.user_identifier] = connection
111
+ end
112
+
113
+ def destroy_user_connection(connection)
114
+ return unless connection.user_connection?
115
+ WebsocketRails.users.delete(connection)
116
+ end
117
+
92
118
  end
93
119
  end
@@ -115,7 +115,7 @@ module WebsocketRails
115
115
  @namespace = validate_namespace( options[:namespace] || namespace )
116
116
  end
117
117
 
118
- def serialize
118
+ def as_json
119
119
  [
120
120
  encoded_name,
121
121
  {
@@ -127,7 +127,11 @@ module WebsocketRails
127
127
  :result => result,
128
128
  :server_token => server_token
129
129
  }
130
- ].to_json
130
+ ]
131
+ end
132
+
133
+ def serialize
134
+ as_json.to_json
131
135
  end
132
136
 
133
137
  def is_channel?
@@ -5,6 +5,22 @@ require "redis/connection/ruby"
5
5
  module WebsocketRails
6
6
  class Synchronization
7
7
 
8
+ def self.all_users
9
+ singleton.all_users
10
+ end
11
+
12
+ def self.find_user(connection)
13
+ singleton.find_user connection
14
+ end
15
+
16
+ def self.register_user(connection)
17
+ singleton.register_user connection
18
+ end
19
+
20
+ def self.destroy_user(connection)
21
+ singleton.destroy_user connection
22
+ end
23
+
8
24
  def self.publish(event)
9
25
  singleton.publish event
10
26
  end
@@ -48,7 +64,7 @@ module WebsocketRails
48
64
 
49
65
  def synchronize!
50
66
  unless @synchronizing
51
- @server_token = generate_unique_token
67
+ @server_token = generate_server_token
52
68
  register_server(@server_token)
53
69
 
54
70
  synchro = Fiber.new do
@@ -94,7 +110,7 @@ module WebsocketRails
94
110
  when event.is_channel?
95
111
  WebsocketRails[event.channel].trigger_event(event)
96
112
  when event.is_user?
97
- connection = WebsocketRails.users[event.user_id]
113
+ connection = WebsocketRails.users[event.user_id.to_s]
98
114
  return if connection.nil?
99
115
  connection.trigger event
100
116
  end
@@ -104,7 +120,7 @@ module WebsocketRails
104
120
  remove_server(server_token)
105
121
  end
106
122
 
107
- def generate_unique_token
123
+ def generate_server_token
108
124
  begin
109
125
  token = SecureRandom.urlsafe_base64
110
126
  end while redis.sismember("websocket_rails.active_servers", token)
@@ -125,5 +141,34 @@ module WebsocketRails
125
141
  EM.stop
126
142
  end
127
143
 
144
+ def register_user(connection)
145
+ Fiber.new do
146
+ id = connection.user_identifier
147
+ user = connection.user
148
+ redis.hset 'websocket_rails.users', id, user.as_json(root: false).to_json
149
+ end.resume
150
+ end
151
+
152
+ def destroy_user(identifier)
153
+ Fiber.new do
154
+ redis.hdel 'websocket_rails.users', identifier
155
+ end.resume
156
+ end
157
+
158
+ def find_user(identifier)
159
+ Fiber.new do
160
+ redis_client = EM.reactor_running? ? redis : ruby_redis
161
+ raw_user = redis_client.hget('websocket_rails.users', identifier)
162
+ raw_user ? JSON.parse(raw_user) : nil
163
+ end.resume
164
+ end
165
+
166
+ def all_users
167
+ Fiber.new do
168
+ redis_client = EM.reactor_running? ? redis : ruby_redis
169
+ redis_client.hgetall('websocket_rails.users')
170
+ end.resume
171
+ end
172
+
128
173
  end
129
174
  end
@@ -27,8 +27,8 @@ module WebsocketRails
27
27
  #
28
28
  # If no `current_user` method is defined or the
29
29
  # user is not signed in when the WebsocketRails
30
- # connection is opened, the key will default to
31
- # `connection.id`.
30
+ # connection is opened, the connection will not be
31
+ # stored in the UserManager.
32
32
  def self.users
33
33
  @user_manager ||= UserManager.new
34
34
  end
@@ -42,37 +42,228 @@ module WebsocketRails
42
42
  end
43
43
 
44
44
  def [](identifier)
45
- unless user = @users[identifier]
46
- user = MissingUser.new(identifier)
45
+ unless user = (@users[identifier.to_s] || find_remote_user(identifier.to_s))
46
+ user = MissingConnection.new(identifier.to_s)
47
47
  end
48
48
  user
49
49
  end
50
50
 
51
51
  def []=(identifier, connection)
52
- @users[identifier] = connection
52
+ @users[identifier.to_s] ||= LocalConnection.new
53
+ @users[identifier.to_s] << connection
54
+ Synchronization.register_user(connection) if WebsocketRails.synchronize?
53
55
  end
54
56
 
55
- def delete(identifier)
56
- @users.delete(identifier)
57
+ def delete(connection)
58
+ identifier = connection.user_identifier.to_s
59
+
60
+ if (@users.has_key?(identifier) && @users[identifier].connections.count > 1)
61
+ @users[identifier].delete(connection)
62
+ else
63
+ @users.delete(identifier)
64
+ Synchronization.destroy_user(identifier) if WebsocketRails.synchronize?
65
+ end
57
66
  end
58
67
 
59
- class MissingUser
68
+ # Behaves similarly to Ruby's Array#each, yielding each connection
69
+ # object stored in the {UserManager}. If synchronization is enabled,
70
+ # each connection from every active worker will be yielded.
71
+ #
72
+ # You can access the `current_user` object through the #user method.
73
+ #
74
+ # You can trigger an event on this user using the #send_message method
75
+ # which behaves identically to BaseController#send_message.
76
+ #
77
+ # If Synchronization is enabled, the state of the `current_user` object
78
+ # will be equivalent to it's state at the time the connection was opened.
79
+ # It will not reflect changes made after the connection has been opened.
80
+ def each(&block)
81
+ if WebsocketRails.synchronize?
82
+ users_hash = Synchronization.all_users || return
83
+ users_hash.each do |identifier, user_json|
84
+ connection = remote_connection_from_json(identifier, user_json)
85
+ block.call(connection) if block
86
+ end
87
+ else
88
+ users.each do |_, connection|
89
+ block.call(connection) if block
90
+ end
91
+ end
92
+ end
60
93
 
61
- def initialize(identifier)
62
- @identifier = identifier
94
+ # Behaves similarly to Ruby's Array#map, invoking the given block with
95
+ # each active connection object and returning a new array with the results.
96
+ #
97
+ # See UserManager#each for details on the current usage and limitations.
98
+ def map(&block)
99
+ collection = []
100
+
101
+ each do |connection|
102
+ collection << block.call(connection) if block
103
+ end
104
+
105
+ collection
106
+ end
107
+
108
+ private
109
+
110
+ def find_remote_user(identifier)
111
+ return unless WebsocketRails.synchronize?
112
+ user_hash = Synchronization.find_user(identifier) || return
113
+
114
+ remote_connection identifier, user_hash
115
+ end
116
+
117
+ def remote_connection_from_json(identifier, user_json)
118
+ user_hash = JSON.parse(user_json)
119
+ remote_connection identifier, user_hash
120
+ end
121
+
122
+ def remote_connection(identifier, user_hash)
123
+ RemoteConnection.new identifier, user_hash
124
+ end
125
+
126
+ # The UserManager::LocalConnection Class serves as a proxy object
127
+ # for storing multiple connections that belong to the same
128
+ # user. It implements the same basic interface as a Connection.
129
+ # This allows you to work with the object as though it is a
130
+ # single connection, but still trigger the events on all
131
+ # active connections belonging to the user.
132
+ class LocalConnection
133
+
134
+ attr_reader :connections
135
+
136
+ def initialize
137
+ @connections = []
138
+ end
139
+
140
+ def <<(connection)
141
+ @connections << connection
142
+ end
143
+
144
+ def delete(connection)
145
+ @connections.delete(connection)
146
+ end
147
+
148
+ def connected?
149
+ true
150
+ end
151
+
152
+ def user_identifier
153
+ latest_connection.user_identifier
154
+ end
155
+
156
+ def user
157
+ latest_connection.user
158
+ end
159
+
160
+ def trigger(event)
161
+ connections.each do |connection|
162
+ connection.trigger event
163
+ end
63
164
  end
64
165
 
65
166
  def send_message(event_name, data = {}, options = {})
66
- if WebsocketRails.synchronize?
67
- options.merge! :user_id => @identifier
68
- options[:data] = data
69
-
70
- event = Event.new(event_name, options)
71
- Synchronization.publish event
72
- true
73
- else
74
- false
167
+ options.merge! :user_id => user_identifier
168
+ options[:data] = data
169
+
170
+ event = Event.new(event_name, options)
171
+
172
+ # Trigger the event on all active connections for this user.
173
+ connections.each do |connection|
174
+ connection.trigger event
75
175
  end
176
+
177
+ # Still publish the event in case the user is connected to
178
+ # other workers as well.
179
+ Synchronization.publish event if WebsocketRails.synchronize?
180
+ true
181
+ end
182
+
183
+ private
184
+
185
+ def latest_connection
186
+ @connections.last
187
+ end
188
+
189
+ end
190
+
191
+ class RemoteConnection
192
+
193
+ attr_reader :user_identifier, :user
194
+
195
+ def initialize(identifier, user_hash)
196
+ @user_identifier = identifier.to_s
197
+ @user_hash = user_hash
198
+ end
199
+
200
+ def connected?
201
+ true
202
+ end
203
+
204
+ def user
205
+ @user ||= load_user
206
+ end
207
+
208
+ def send_message(event_name, data = {}, options = {})
209
+ options.merge! :user_id => @user_identifier
210
+ options[:data] = data
211
+
212
+ event = Event.new(event_name, options)
213
+
214
+ # If the user is connected to this worker, trigger the event
215
+ # immediately as the event will be ignored by the Synchronization
216
+ ## dispatcher since the server_token will match.
217
+ if connection = WebsocketRails.users.users[@user_identifier]
218
+ connection.trigger event
219
+ end
220
+
221
+ # Still publish the event in case the user is connected to
222
+ # other workers as well.
223
+ #
224
+ # No need to check for Synchronization being enabled here.
225
+ # If a RemoteConnection has been fetched, Synchronization
226
+ # must be enabled.
227
+ Synchronization.publish event
228
+ true
229
+ end
230
+
231
+ private
232
+
233
+ def load_user
234
+ user = WebsocketRails.config.user_class.new
235
+ set_user_attributes user, @user_hash
236
+ user
237
+ end
238
+
239
+ def set_user_attributes(user, attr)
240
+ attr.each do |k, v|
241
+ user.send "#{k}=", v
242
+ end
243
+ user.instance_variable_set(:@new_record, false)
244
+ user.instance_variable_set(:@destroyed, false)
245
+ end
246
+
247
+ end
248
+
249
+ class MissingConnection
250
+
251
+ attr_reader :identifier
252
+
253
+ def initialize(identifier)
254
+ @user_identifier = identifier.to_s
255
+ end
256
+
257
+ def connected?
258
+ false
259
+ end
260
+
261
+ def user
262
+ nil
263
+ end
264
+
265
+ def send_message(*args)
266
+ false
76
267
  end
77
268
 
78
269
  def nil?