omg-actioncable 8.0.0.alpha2

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