omg-actioncable 8.0.0.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|