omg-actioncable 8.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +511 -0
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +335 -0
  9. data/lib/action_cable/channel/broadcasting.rb +50 -0
  10. data/lib/action_cable/channel/callbacks.rb +76 -0
  11. data/lib/action_cable/channel/naming.rb +28 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +215 -0
  14. data/lib/action_cable/channel/test_case.rb +356 -0
  15. data/lib/action_cable/connection/authorization.rb +18 -0
  16. data/lib/action_cable/connection/base.rb +294 -0
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +159 -0
  19. data/lib/action_cable/connection/identification.rb +51 -0
  20. data/lib/action_cable/connection/internal_channel.rb +50 -0
  21. data/lib/action_cable/connection/message_buffer.rb +57 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +85 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
  26. data/lib/action_cable/connection/test_case.rb +246 -0
  27. data/lib/action_cable/connection/web_socket.rb +45 -0
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +98 -0
  30. data/lib/action_cable/gem_version.rb +19 -0
  31. data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
  32. data/lib/action_cable/remote_connections.rb +82 -0
  33. data/lib/action_cable/server/base.rb +109 -0
  34. data/lib/action_cable/server/broadcasting.rb +62 -0
  35. data/lib/action_cable/server/configuration.rb +70 -0
  36. data/lib/action_cable/server/connections.rb +44 -0
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
  38. data/lib/action_cable/server/worker.rb +75 -0
  39. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  40. data/lib/action_cable/subscription_adapter/base.rb +36 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +39 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
  44. data/lib/action_cable/subscription_adapter/redis.rb +256 -0
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +41 -0
  47. data/lib/action_cable/test_case.rb +13 -0
  48. data/lib/action_cable/test_helper.rb +163 -0
  49. data/lib/action_cable/version.rb +12 -0
  50. data/lib/action_cable.rb +80 -0
  51. data/lib/rails/generators/channel/USAGE +19 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +127 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +181 -0
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "action_dispatch"
6
+ require "active_support/rescuable"
7
+
8
+ module ActionCable
9
+ module Connection
10
+ # # Action Cable Connection Base
11
+ #
12
+ # For every WebSocket connection the Action Cable server accepts, a Connection
13
+ # object will be instantiated. This instance becomes the parent of all of the
14
+ # channel subscriptions that are created from there on. Incoming messages are
15
+ # then routed to these channel subscriptions based on an identifier sent by the
16
+ # Action Cable consumer. The Connection itself does not deal with any specific
17
+ # application logic beyond authentication and authorization.
18
+ #
19
+ # Here's a basic example:
20
+ #
21
+ # module ApplicationCable
22
+ # class Connection < ActionCable::Connection::Base
23
+ # identified_by :current_user
24
+ #
25
+ # def connect
26
+ # self.current_user = find_verified_user
27
+ # logger.add_tags current_user.name
28
+ # end
29
+ #
30
+ # def disconnect
31
+ # # Any cleanup work needed when the cable connection is cut.
32
+ # end
33
+ #
34
+ # private
35
+ # def find_verified_user
36
+ # User.find_by_identity(cookies.encrypted[:identity_id]) ||
37
+ # reject_unauthorized_connection
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # First, we declare that this connection can be identified by its current_user.
43
+ # This allows us to later be able to find all connections established for that
44
+ # current_user (and potentially disconnect them). You can declare as many
45
+ # identification indexes as you like. Declaring an identification means that an
46
+ # attr_accessor is automatically set for that key.
47
+ #
48
+ # Second, we rely on the fact that the WebSocket connection is established with
49
+ # the cookies from the domain being sent along. This makes it easy to use signed
50
+ # cookies that were set when logging in via a web interface to authorize the
51
+ # WebSocket connection.
52
+ #
53
+ # Finally, we add a tag to the connection-specific logger with the name of the
54
+ # current user to easily distinguish their messages in the log.
55
+ #
56
+ # Pretty simple, eh?
57
+ class Base
58
+ include Identification
59
+ include InternalChannel
60
+ include Authorization
61
+ include Callbacks
62
+ include ActiveSupport::Rescuable
63
+
64
+ attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
65
+ delegate :event_loop, :pubsub, :config, to: :server
66
+
67
+ def initialize(server, env, coder: ActiveSupport::JSON)
68
+ @server, @env, @coder = server, env, coder
69
+
70
+ @worker_pool = server.worker_pool
71
+ @logger = new_tagged_logger
72
+
73
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
74
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
75
+ @message_buffer = ActionCable::Connection::MessageBuffer.new(self)
76
+
77
+ @_internal_subscriptions = nil
78
+ @started_at = Time.now
79
+ end
80
+
81
+ # Called by the server when a new WebSocket connection is established. This
82
+ # configures the callbacks intended for overwriting by the user. This method
83
+ # should not be called directly -- instead rely upon on the #connect (and
84
+ # #disconnect) callbacks.
85
+ def process # :nodoc:
86
+ logger.info started_request_message
87
+
88
+ if websocket.possible? && allow_request_origin?
89
+ respond_to_successful_request
90
+ else
91
+ respond_to_invalid_request
92
+ end
93
+ end
94
+
95
+ # Decodes WebSocket messages and dispatches them to subscribed channels.
96
+ # WebSocket message transfer encoding is always JSON.
97
+ def receive(websocket_message) # :nodoc:
98
+ send_async :dispatch_websocket_message, websocket_message
99
+ end
100
+
101
+ def dispatch_websocket_message(websocket_message) # :nodoc:
102
+ if websocket.alive?
103
+ handle_channel_command decode(websocket_message)
104
+ else
105
+ logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
106
+ end
107
+ end
108
+
109
+ def handle_channel_command(payload)
110
+ run_callbacks :command do
111
+ subscriptions.execute_command payload
112
+ end
113
+ end
114
+
115
+ def transmit(cable_message) # :nodoc:
116
+ websocket.transmit encode(cable_message)
117
+ end
118
+
119
+ # Close the WebSocket connection.
120
+ def close(reason: nil, reconnect: true)
121
+ transmit(
122
+ type: ActionCable::INTERNAL[:message_types][:disconnect],
123
+ reason: reason,
124
+ reconnect: reconnect
125
+ )
126
+ websocket.close
127
+ end
128
+
129
+ # Invoke a method on the connection asynchronously through the pool of thread
130
+ # workers.
131
+ def send_async(method, *arguments)
132
+ worker_pool.async_invoke(self, method, *arguments)
133
+ end
134
+
135
+ # Return a basic hash of statistics for the connection keyed with `identifier`,
136
+ # `started_at`, `subscriptions`, and `request_id`. This can be returned by a
137
+ # health check against the connection.
138
+ def statistics
139
+ {
140
+ identifier: connection_identifier,
141
+ started_at: @started_at,
142
+ subscriptions: subscriptions.identifiers,
143
+ request_id: @env["action_dispatch.request_id"]
144
+ }
145
+ end
146
+
147
+ def beat
148
+ transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i
149
+ end
150
+
151
+ def on_open # :nodoc:
152
+ send_async :handle_open
153
+ end
154
+
155
+ def on_message(message) # :nodoc:
156
+ message_buffer.append message
157
+ end
158
+
159
+ def on_error(message) # :nodoc:
160
+ # log errors to make diagnosing socket errors easier
161
+ logger.error "WebSocket error occurred: #{message}"
162
+ end
163
+
164
+ def on_close(reason, code) # :nodoc:
165
+ send_async :handle_close
166
+ end
167
+
168
+ def inspect # :nodoc:
169
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
170
+ end
171
+
172
+ private
173
+ attr_reader :websocket
174
+ attr_reader :message_buffer
175
+
176
+ # The request that initiated the WebSocket connection is available here. This
177
+ # gives access to the environment, cookies, etc.
178
+ def request # :doc:
179
+ @request ||= begin
180
+ environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
181
+ ActionDispatch::Request.new(environment || env)
182
+ end
183
+ end
184
+
185
+ # The cookies of the request that initiated the WebSocket connection. Useful for
186
+ # performing authorization checks.
187
+ def cookies # :doc:
188
+ request.cookie_jar
189
+ end
190
+
191
+ def encode(cable_message)
192
+ @coder.encode cable_message
193
+ end
194
+
195
+ def decode(websocket_message)
196
+ @coder.decode websocket_message
197
+ end
198
+
199
+ def handle_open
200
+ @protocol = websocket.protocol
201
+ connect if respond_to?(:connect)
202
+ subscribe_to_internal_channel
203
+ send_welcome_message
204
+
205
+ message_buffer.process!
206
+ server.add_connection(self)
207
+ rescue ActionCable::Connection::Authorization::UnauthorizedError
208
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
209
+ end
210
+
211
+ def handle_close
212
+ logger.info finished_request_message
213
+
214
+ server.remove_connection(self)
215
+
216
+ subscriptions.unsubscribe_from_all
217
+ unsubscribe_from_internal_channel
218
+
219
+ disconnect if respond_to?(:disconnect)
220
+ end
221
+
222
+ def send_welcome_message
223
+ # Send welcome message to the internal connection monitor channel. This ensures
224
+ # the connection monitor state is reset after a successful websocket connection.
225
+ transmit type: ActionCable::INTERNAL[:message_types][:welcome]
226
+ end
227
+
228
+ def allow_request_origin?
229
+ return true if server.config.disable_request_forgery_protection
230
+
231
+ proto = Rack::Request.new(env).ssl? ? "https" : "http"
232
+ if server.config.allow_same_origin_as_host && env["HTTP_ORIGIN"] == "#{proto}://#{env['HTTP_HOST']}"
233
+ true
234
+ elsif Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env["HTTP_ORIGIN"] }
235
+ true
236
+ else
237
+ logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}")
238
+ false
239
+ end
240
+ end
241
+
242
+ def respond_to_successful_request
243
+ logger.info successful_request_message
244
+ websocket.rack_response
245
+ end
246
+
247
+ def respond_to_invalid_request
248
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
249
+
250
+ logger.error invalid_request_message
251
+ logger.info finished_request_message
252
+ [ 404, { Rack::CONTENT_TYPE => "text/plain; charset=utf-8" }, [ "Page not found" ] ]
253
+ end
254
+
255
+ # Tags are declared in the server but computed in the connection. This allows us
256
+ # per-connection tailored tags.
257
+ def new_tagged_logger
258
+ TaggedLoggerProxy.new server.logger,
259
+ tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize }
260
+ end
261
+
262
+ def started_request_message
263
+ 'Started %s "%s"%s for %s at %s' % [
264
+ request.request_method,
265
+ request.filtered_path,
266
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
267
+ request.ip,
268
+ Time.now.to_s ]
269
+ end
270
+
271
+ def finished_request_message
272
+ 'Finished "%s"%s for %s at %s' % [
273
+ request.filtered_path,
274
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
275
+ request.ip,
276
+ Time.now.to_s ]
277
+ end
278
+
279
+ def invalid_request_message
280
+ "Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
281
+ env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
282
+ ]
283
+ end
284
+
285
+ def successful_request_message
286
+ "Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
287
+ env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
288
+ ]
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/callbacks"
6
+
7
+ module ActionCable
8
+ module Connection
9
+ # # Action Cable Connection Callbacks
10
+ #
11
+ # The [before_command](rdoc-ref:ClassMethods#before_command),
12
+ # [after_command](rdoc-ref:ClassMethods#after_command), and
13
+ # [around_command](rdoc-ref:ClassMethods#around_command) callbacks are invoked
14
+ # when sending commands to the client, such as when subscribing, unsubscribing,
15
+ # or performing an action.
16
+ #
17
+ # #### Example
18
+ #
19
+ # module ApplicationCable
20
+ # class Connection < ActionCable::Connection::Base
21
+ # identified_by :user
22
+ #
23
+ # around_command :set_current_account
24
+ #
25
+ # private
26
+ #
27
+ # def set_current_account
28
+ # # Now all channels could use Current.account
29
+ # Current.set(account: user.account) { yield }
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ module Callbacks
35
+ extend ActiveSupport::Concern
36
+ include ActiveSupport::Callbacks
37
+
38
+ included do
39
+ define_callbacks :command
40
+ end
41
+
42
+ module ClassMethods
43
+ def before_command(*methods, &block)
44
+ set_callback(:command, :before, *methods, &block)
45
+ end
46
+
47
+ def after_command(*methods, &block)
48
+ set_callback(:command, :after, *methods, &block)
49
+ end
50
+
51
+ def around_command(*methods, &block)
52
+ set_callback(:command, :around, *methods, &block)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "websocket/driver"
6
+
7
+ module ActionCable
8
+ module Connection
9
+ #--
10
+ # This class is heavily based on faye-websocket-ruby
11
+ #
12
+ # Copyright (c) 2010-2015 James Coglan
13
+ class ClientSocket # :nodoc:
14
+ def self.determine_url(env)
15
+ scheme = secure_request?(env) ? "wss:" : "ws:"
16
+ "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
17
+ end
18
+
19
+ def self.secure_request?(env)
20
+ return true if env["HTTPS"] == "on"
21
+ return true if env["HTTP_X_FORWARDED_SSL"] == "on"
22
+ return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
23
+ return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
24
+ return true if env["rack.url_scheme"] == "https"
25
+
26
+ false
27
+ end
28
+
29
+ CONNECTING = 0
30
+ OPEN = 1
31
+ CLOSING = 2
32
+ CLOSED = 3
33
+
34
+ attr_reader :env, :url
35
+
36
+ def initialize(env, event_target, event_loop, protocols)
37
+ @env = env
38
+ @event_target = event_target
39
+ @event_loop = event_loop
40
+
41
+ @url = ClientSocket.determine_url(@env)
42
+
43
+ @driver = @driver_started = nil
44
+ @close_params = ["", 1006]
45
+
46
+ @ready_state = CONNECTING
47
+
48
+ # The driver calls `env`, `url`, and `write`
49
+ @driver = ::WebSocket::Driver.rack(self, protocols: protocols)
50
+
51
+ @driver.on(:open) { |e| open }
52
+ @driver.on(:message) { |e| receive_message(e.data) }
53
+ @driver.on(:close) { |e| begin_close(e.reason, e.code) }
54
+ @driver.on(:error) { |e| emit_error(e.message) }
55
+
56
+ @stream = ActionCable::Connection::Stream.new(@event_loop, self)
57
+ end
58
+
59
+ def start_driver
60
+ return if @driver.nil? || @driver_started
61
+ @stream.hijack_rack_socket
62
+
63
+ if callback = @env["async.callback"]
64
+ callback.call([101, {}, @stream])
65
+ end
66
+
67
+ @driver_started = true
68
+ @driver.start
69
+ end
70
+
71
+ def rack_response
72
+ start_driver
73
+ [ -1, {}, [] ]
74
+ end
75
+
76
+ def write(data)
77
+ @stream.write(data)
78
+ rescue => e
79
+ emit_error e.message
80
+ end
81
+
82
+ def transmit(message)
83
+ return false if @ready_state > OPEN
84
+ case message
85
+ when Numeric then @driver.text(message.to_s)
86
+ when String then @driver.text(message)
87
+ when Array then @driver.binary(message)
88
+ else false
89
+ end
90
+ end
91
+
92
+ def close(code = nil, reason = nil)
93
+ code ||= 1000
94
+ reason ||= ""
95
+
96
+ unless code == 1000 || (code >= 3000 && code <= 4999)
97
+ raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
98
+ "The code must be either 1000, or between 3000 and 4999. " \
99
+ "#{code} is neither."
100
+ end
101
+
102
+ @ready_state = CLOSING unless @ready_state == CLOSED
103
+ @driver.close(reason, code)
104
+ end
105
+
106
+ def parse(data)
107
+ @driver.parse(data)
108
+ end
109
+
110
+ def client_gone
111
+ finalize_close
112
+ end
113
+
114
+ def alive?
115
+ @ready_state == OPEN
116
+ end
117
+
118
+ def protocol
119
+ @driver.protocol
120
+ end
121
+
122
+ private
123
+ def open
124
+ return unless @ready_state == CONNECTING
125
+ @ready_state = OPEN
126
+
127
+ @event_target.on_open
128
+ end
129
+
130
+ def receive_message(data)
131
+ return unless @ready_state == OPEN
132
+
133
+ @event_target.on_message(data)
134
+ end
135
+
136
+ def emit_error(message)
137
+ return if @ready_state >= CLOSING
138
+
139
+ @event_target.on_error(message)
140
+ end
141
+
142
+ def begin_close(reason, code)
143
+ return if @ready_state == CLOSED
144
+ @ready_state = CLOSING
145
+ @close_params = [reason, code]
146
+
147
+ @stream.shutdown if @stream
148
+ finalize_close
149
+ end
150
+
151
+ def finalize_close
152
+ return if @ready_state == CLOSED
153
+ @ready_state = CLOSED
154
+
155
+ @event_target.on_close(*@close_params)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "set"
6
+
7
+ module ActionCable
8
+ module Connection
9
+ module Identification
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class_attribute :identifiers, default: Set.new
14
+ end
15
+
16
+ module ClassMethods
17
+ # Mark a key as being a connection identifier index that can then be used to
18
+ # find the specific connection again later. Common identifiers are current_user
19
+ # and current_account, but could be anything, really.
20
+ #
21
+ # Note that anything marked as an identifier will automatically create a
22
+ # delegate by the same name on any channel instances created off the connection.
23
+ def identified_by(*identifiers)
24
+ Array(identifiers).each { |identifier| attr_accessor identifier }
25
+ self.identifiers += identifiers
26
+ end
27
+ end
28
+
29
+ # Return a single connection identifier that combines the value of all the
30
+ # registered identifiers into a single gid.
31
+ def connection_identifier
32
+ unless defined? @connection_identifier
33
+ @connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") }
34
+ end
35
+
36
+ @connection_identifier
37
+ end
38
+
39
+ private
40
+ def connection_gid(ids)
41
+ ids.map do |o|
42
+ if o.respond_to? :to_gid_param
43
+ o.to_gid_param
44
+ else
45
+ o.to_s
46
+ end
47
+ end.sort.join(":")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Connection
7
+ # # Action Cable InternalChannel
8
+ #
9
+ # Makes it possible for the RemoteConnection to disconnect a specific
10
+ # connection.
11
+ module InternalChannel
12
+ extend ActiveSupport::Concern
13
+
14
+ private
15
+ def internal_channel
16
+ "action_cable/#{connection_identifier}"
17
+ end
18
+
19
+ def subscribe_to_internal_channel
20
+ if connection_identifier.present?
21
+ callback = -> (message) { process_internal_message decode(message) }
22
+ @_internal_subscriptions ||= []
23
+ @_internal_subscriptions << [ internal_channel, callback ]
24
+
25
+ server.event_loop.post { pubsub.subscribe(internal_channel, callback) }
26
+ logger.info "Registered connection (#{connection_identifier})"
27
+ end
28
+ end
29
+
30
+ def unsubscribe_from_internal_channel
31
+ if @_internal_subscriptions.present?
32
+ @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } }
33
+ end
34
+ end
35
+
36
+ def process_internal_message(message)
37
+ case message["type"]
38
+ when "disconnect"
39
+ logger.info "Removing connection (#{connection_identifier})"
40
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:remote], reconnect: message.fetch("reconnect", true))
41
+ end
42
+ rescue Exception => e
43
+ logger.error "There was an exception - #{e.class}(#{e.message})"
44
+ logger.error e.backtrace.join("\n")
45
+
46
+ close
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Connection
7
+ # Allows us to buffer messages received from the WebSocket before the Connection
8
+ # has been fully initialized, and is ready to receive them.
9
+ class MessageBuffer # :nodoc:
10
+ def initialize(connection)
11
+ @connection = connection
12
+ @buffered_messages = []
13
+ end
14
+
15
+ def append(message)
16
+ if valid? message
17
+ if processing?
18
+ receive message
19
+ else
20
+ buffer message
21
+ end
22
+ else
23
+ connection.logger.error "Couldn't handle non-string message: #{message.class}"
24
+ end
25
+ end
26
+
27
+ def processing?
28
+ @processing
29
+ end
30
+
31
+ def process!
32
+ @processing = true
33
+ receive_buffered_messages
34
+ end
35
+
36
+ private
37
+ attr_reader :connection
38
+ attr_reader :buffered_messages
39
+
40
+ def valid?(message)
41
+ message.is_a?(String)
42
+ end
43
+
44
+ def receive(message)
45
+ connection.receive message
46
+ end
47
+
48
+ def buffer(message)
49
+ buffered_messages << message
50
+ end
51
+
52
+ def receive_buffered_messages
53
+ receive buffered_messages.shift until buffered_messages.empty?
54
+ end
55
+ end
56
+ end
57
+ end