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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +24 -0
- data/app/assets/javascripts/action_cable.js +511 -0
- data/app/assets/javascripts/actioncable.esm.js +512 -0
- data/app/assets/javascripts/actioncable.js +510 -0
- data/lib/action_cable/channel/base.rb +335 -0
- data/lib/action_cable/channel/broadcasting.rb +50 -0
- data/lib/action_cable/channel/callbacks.rb +76 -0
- data/lib/action_cable/channel/naming.rb +28 -0
- data/lib/action_cable/channel/periodic_timers.rb +78 -0
- data/lib/action_cable/channel/streams.rb +215 -0
- data/lib/action_cable/channel/test_case.rb +356 -0
- data/lib/action_cable/connection/authorization.rb +18 -0
- data/lib/action_cable/connection/base.rb +294 -0
- data/lib/action_cable/connection/callbacks.rb +57 -0
- data/lib/action_cable/connection/client_socket.rb +159 -0
- data/lib/action_cable/connection/identification.rb +51 -0
- data/lib/action_cable/connection/internal_channel.rb +50 -0
- data/lib/action_cable/connection/message_buffer.rb +57 -0
- data/lib/action_cable/connection/stream.rb +117 -0
- data/lib/action_cable/connection/stream_event_loop.rb +136 -0
- data/lib/action_cable/connection/subscriptions.rb +85 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
- data/lib/action_cable/connection/test_case.rb +246 -0
- data/lib/action_cable/connection/web_socket.rb +45 -0
- data/lib/action_cable/deprecator.rb +9 -0
- data/lib/action_cable/engine.rb +98 -0
- data/lib/action_cable/gem_version.rb +19 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
- data/lib/action_cable/remote_connections.rb +82 -0
- data/lib/action_cable/server/base.rb +109 -0
- data/lib/action_cable/server/broadcasting.rb +62 -0
- data/lib/action_cable/server/configuration.rb +70 -0
- data/lib/action_cable/server/connections.rb +44 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/subscription_adapter/async.rb +29 -0
- data/lib/action_cable/subscription_adapter/base.rb +36 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
- data/lib/action_cable/subscription_adapter/inline.rb +39 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
- data/lib/action_cable/subscription_adapter/redis.rb +256 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
- data/lib/action_cable/subscription_adapter/test.rb +41 -0
- data/lib/action_cable/test_case.rb +13 -0
- data/lib/action_cable/test_helper.rb +163 -0
- data/lib/action_cable/version.rb +12 -0
- data/lib/action_cable.rb +80 -0
- data/lib/rails/generators/channel/USAGE +19 -0
- data/lib/rails/generators/channel/channel_generator.rb +127 -0
- data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
- data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
- data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- 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
|