websocket-rails 0.5.0 → 0.6.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.
@@ -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?