actioncable-next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +17 -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 +81 -0
- data/lib/action_cable/channel/streams.rb +213 -0
- data/lib/action_cable/channel/test_case.rb +329 -0
- data/lib/action_cable/connection/authorization.rb +18 -0
- data/lib/action_cable/connection/base.rb +165 -0
- data/lib/action_cable/connection/callbacks.rb +57 -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/subscriptions.rb +124 -0
- data/lib/action_cable/connection/test_case.rb +294 -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 +163 -0
- data/lib/action_cable/server/broadcasting.rb +62 -0
- data/lib/action_cable/server/configuration.rb +75 -0
- data/lib/action_cable/server/connections.rb +44 -0
- data/lib/action_cable/server/socket/client_socket.rb +159 -0
- data/lib/action_cable/server/socket/message_buffer.rb +56 -0
- data/lib/action_cable/server/socket/stream.rb +117 -0
- data/lib/action_cable/server/socket/web_socket.rb +47 -0
- data/lib/action_cable/server/socket.rb +180 -0
- data/lib/action_cable/server/stream_event_loop.rb +119 -0
- data/lib/action_cable/server/tagged_logger_proxy.rb +46 -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 +14 -0
- data/lib/action_cable/subscription_adapter/base.rb +39 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
- data/lib/action_cable/subscription_adapter/inline.rb +40 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +130 -0
- data/lib/action_cable/subscription_adapter/redis.rb +257 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +80 -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 +81 -0
- data/lib/actioncable-next.rb +5 -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 +191 -0
@@ -0,0 +1,294 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support"
|
6
|
+
require "active_support/test_case"
|
7
|
+
require "active_support/core_ext/hash/indifferent_access"
|
8
|
+
require "action_dispatch"
|
9
|
+
require "action_dispatch/http/headers"
|
10
|
+
require "action_dispatch/testing/test_request"
|
11
|
+
|
12
|
+
module ActionCable
|
13
|
+
module Connection
|
14
|
+
class NonInferrableConnectionError < ::StandardError
|
15
|
+
def initialize(name)
|
16
|
+
super "Unable to determine the connection to test from #{name}. " +
|
17
|
+
"You'll need to specify it using `tests YourConnection` in your " +
|
18
|
+
"test case definition."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Assertions
|
23
|
+
# Asserts that the connection is rejected (via
|
24
|
+
# `reject_unauthorized_connection`).
|
25
|
+
#
|
26
|
+
# # Asserts that connection without user_id fails
|
27
|
+
# assert_reject_connection { connect params: { user_id: '' } }
|
28
|
+
def assert_reject_connection(&block)
|
29
|
+
assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class TestCookies < ActiveSupport::HashWithIndifferentAccess # :nodoc:
|
34
|
+
def []=(name, options)
|
35
|
+
value = options.is_a?(Hash) ? options.symbolize_keys[:value] : options
|
36
|
+
super(name, value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# We don't want to use the whole "encryption stack" for connection unit-tests,
|
41
|
+
# but we want to make sure that users test against the correct types of cookies
|
42
|
+
# (i.e. signed or encrypted or plain)
|
43
|
+
class TestCookieJar < TestCookies
|
44
|
+
def signed
|
45
|
+
@signed ||= TestCookies.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def encrypted
|
49
|
+
@encrypted ||= TestCookies.new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class TestSocket
|
54
|
+
# Make session and cookies available to the connection
|
55
|
+
class Request < ActionDispatch::TestRequest
|
56
|
+
attr_accessor :session, :cookie_jar
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :logger, :request, :transmissions, :closed, :env
|
60
|
+
|
61
|
+
class << self
|
62
|
+
def build_request(path, params: nil, headers: {}, session: {}, env: {}, cookies: nil)
|
63
|
+
wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
|
64
|
+
|
65
|
+
uri = URI.parse(path)
|
66
|
+
|
67
|
+
query_string = params.nil? ? uri.query : params.to_query
|
68
|
+
|
69
|
+
request_env = {
|
70
|
+
"QUERY_STRING" => query_string,
|
71
|
+
"PATH_INFO" => uri.path
|
72
|
+
}.merge(env)
|
73
|
+
|
74
|
+
if wrapped_headers.present?
|
75
|
+
ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
|
76
|
+
end
|
77
|
+
|
78
|
+
Request.create(request_env).tap do |request|
|
79
|
+
request.session = session.with_indifferent_access
|
80
|
+
request.cookie_jar = cookies
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def initialize(request)
|
86
|
+
inner_logger = ActiveSupport::Logger.new(StringIO.new)
|
87
|
+
tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
|
88
|
+
@logger = ActionCable::Server::TaggedLoggerProxy.new(tagged_logging, tags: [])
|
89
|
+
@request = request
|
90
|
+
@env = request.env
|
91
|
+
@connection = nil
|
92
|
+
@closed = false
|
93
|
+
@transmissions = []
|
94
|
+
end
|
95
|
+
|
96
|
+
def transmit(data)
|
97
|
+
@transmissions << data.with_indifferent_access
|
98
|
+
end
|
99
|
+
|
100
|
+
def close
|
101
|
+
@closed = true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# TestServer provides test pub/sub and executor implementations
|
106
|
+
class TestServer
|
107
|
+
attr_reader :streams, :config
|
108
|
+
|
109
|
+
def initialize(server)
|
110
|
+
@streams = Hash.new { |h, k| h[k] = [] }
|
111
|
+
@config = server.config
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :pubsub, :itself
|
115
|
+
alias_method :executor, :itself
|
116
|
+
|
117
|
+
#== Executor interface ==
|
118
|
+
|
119
|
+
# Inline async calls
|
120
|
+
def post(&work) = work.call
|
121
|
+
# We don't support timers in unit tests yet
|
122
|
+
def timer(_every) = nil
|
123
|
+
|
124
|
+
#== Pub/sub interface ==
|
125
|
+
def subscribe(stream, callback, success_callback = nil)
|
126
|
+
@streams[stream] << callback
|
127
|
+
success_callback&.call
|
128
|
+
end
|
129
|
+
|
130
|
+
def unsubscribe(stream, callback)
|
131
|
+
@streams[stream].delete(callback)
|
132
|
+
@streams.delete(stream) if @streams[stream].empty?
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# # Action Cable Connection TestCase
|
137
|
+
#
|
138
|
+
# Unit test Action Cable connections.
|
139
|
+
#
|
140
|
+
# Useful to check whether a connection's `identified_by` gets assigned properly
|
141
|
+
# and that any improper connection requests are rejected.
|
142
|
+
#
|
143
|
+
# ## Basic example
|
144
|
+
#
|
145
|
+
# Unit tests are written as follows:
|
146
|
+
#
|
147
|
+
# 1. Simulate a connection attempt by calling `connect`.
|
148
|
+
# 2. Assert state, e.g. identifiers, has been assigned.
|
149
|
+
#
|
150
|
+
#
|
151
|
+
# class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
|
152
|
+
# def test_connects_with_proper_cookie
|
153
|
+
# # Simulate the connection request with a cookie.
|
154
|
+
# cookies["user_id"] = users(:john).id
|
155
|
+
#
|
156
|
+
# connect
|
157
|
+
#
|
158
|
+
# # Assert the connection identifier matches the fixture.
|
159
|
+
# assert_equal users(:john).id, connection.user.id
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# def test_rejects_connection_without_proper_cookie
|
163
|
+
# assert_reject_connection { connect }
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# `connect` accepts additional information about the HTTP request with the
|
168
|
+
# `params`, `headers`, `session`, and Rack `env` options.
|
169
|
+
#
|
170
|
+
# def test_connect_with_headers_and_query_string
|
171
|
+
# connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
|
172
|
+
#
|
173
|
+
# assert_equal "1", connection.user.id
|
174
|
+
# assert_equal "secret-my", connection.token
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# def test_connect_with_params
|
178
|
+
# connect params: { user_id: 1 }
|
179
|
+
#
|
180
|
+
# assert_equal "1", connection.user.id
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# You can also set up the correct cookies before the connection request:
|
184
|
+
#
|
185
|
+
# def test_connect_with_cookies
|
186
|
+
# # Plain cookies:
|
187
|
+
# cookies["user_id"] = 1
|
188
|
+
#
|
189
|
+
# # Or signed/encrypted:
|
190
|
+
# # cookies.signed["user_id"] = 1
|
191
|
+
# # cookies.encrypted["user_id"] = 1
|
192
|
+
#
|
193
|
+
# connect
|
194
|
+
#
|
195
|
+
# assert_equal "1", connection.user_id
|
196
|
+
# end
|
197
|
+
#
|
198
|
+
# ## Connection is automatically inferred
|
199
|
+
#
|
200
|
+
# ActionCable::Connection::TestCase will automatically infer the connection
|
201
|
+
# under test from the test class name. If the channel cannot be inferred from
|
202
|
+
# the test class name, you can explicitly set it with `tests`.
|
203
|
+
#
|
204
|
+
# class ConnectionTest < ActionCable::Connection::TestCase
|
205
|
+
# tests ApplicationCable::Connection
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
class TestCase < ActiveSupport::TestCase
|
209
|
+
module Behavior
|
210
|
+
extend ActiveSupport::Concern
|
211
|
+
|
212
|
+
DEFAULT_PATH = "/cable"
|
213
|
+
|
214
|
+
include ActiveSupport::Testing::ConstantLookup
|
215
|
+
include Assertions
|
216
|
+
|
217
|
+
included do
|
218
|
+
class_attribute :_connection_class
|
219
|
+
|
220
|
+
ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
|
221
|
+
end
|
222
|
+
|
223
|
+
module ClassMethods
|
224
|
+
def tests(connection)
|
225
|
+
case connection
|
226
|
+
when String, Symbol
|
227
|
+
self._connection_class = connection.to_s.camelize.constantize
|
228
|
+
when Module
|
229
|
+
self._connection_class = connection
|
230
|
+
else
|
231
|
+
raise NonInferrableConnectionError.new(connection)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def connection_class
|
236
|
+
if connection = self._connection_class
|
237
|
+
connection
|
238
|
+
else
|
239
|
+
tests determine_default_connection(name)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def determine_default_connection(name)
|
244
|
+
connection = determine_constant_from_test_name(name) do |constant|
|
245
|
+
Class === constant && constant < ActionCable::Connection::Base
|
246
|
+
end
|
247
|
+
raise NonInferrableConnectionError.new(name) if connection.nil?
|
248
|
+
connection
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
attr_reader :connection, :socket, :testserver
|
253
|
+
|
254
|
+
# Performs connection attempt to exert #connect on the connection under test.
|
255
|
+
#
|
256
|
+
# Accepts request path as the first argument and the following request options:
|
257
|
+
#
|
258
|
+
# * params – URL parameters (Hash)
|
259
|
+
# * headers – request headers (Hash)
|
260
|
+
# * session – session data (Hash)
|
261
|
+
# * env – additional Rack env configuration (Hash)
|
262
|
+
def connect(path = ActionCable.server.config.mount_path, server: ActionCable.server, **request_params)
|
263
|
+
path ||= DEFAULT_PATH
|
264
|
+
|
265
|
+
@socket = TestSocket.new(TestSocket.build_request(path, **request_params, cookies: cookies))
|
266
|
+
@testserver = Connection::TestServer.new(server)
|
267
|
+
connection = self.class.connection_class.new(@testserver, socket)
|
268
|
+
connection.connect if connection.respond_to?(:connect)
|
269
|
+
|
270
|
+
# Only set instance variable if connected successfully
|
271
|
+
@connection = connection
|
272
|
+
end
|
273
|
+
|
274
|
+
# Exert #disconnect on the connection under test.
|
275
|
+
def disconnect
|
276
|
+
raise "Must be connected!" if connection.nil?
|
277
|
+
|
278
|
+
connection.disconnect if connection.respond_to?(:disconnect)
|
279
|
+
@connection = nil
|
280
|
+
end
|
281
|
+
|
282
|
+
def cookies
|
283
|
+
@cookie_jar ||= TestCookieJar.new
|
284
|
+
end
|
285
|
+
|
286
|
+
def transmissions
|
287
|
+
socket&.transmissions || []
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
include Behavior
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "rails"
|
6
|
+
require "action_cable"
|
7
|
+
require "active_support/core_ext/hash/indifferent_access"
|
8
|
+
|
9
|
+
module ActionCable
|
10
|
+
class Engine < Rails::Engine # :nodoc:
|
11
|
+
config.action_cable = ActiveSupport::OrderedOptions.new
|
12
|
+
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
|
13
|
+
config.action_cable.precompile_assets = true
|
14
|
+
|
15
|
+
initializer "action_cable.deprecator", before: :load_environment_config do |app|
|
16
|
+
app.deprecators[:action_cable] = ActionCable.deprecator
|
17
|
+
end
|
18
|
+
|
19
|
+
initializer "action_cable.helpers" do
|
20
|
+
ActiveSupport.on_load(:action_view) do
|
21
|
+
include ActionCable::Helpers::ActionCableHelper
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
initializer "action_cable.logger" do
|
26
|
+
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
|
27
|
+
end
|
28
|
+
|
29
|
+
initializer "action_cable.health_check_application" do
|
30
|
+
ActiveSupport.on_load(:action_cable) {
|
31
|
+
self.health_check_application = ->(env) { Rails::HealthController.action(:show).call(env) }
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
initializer "action_cable.asset" do
|
36
|
+
config.after_initialize do |app|
|
37
|
+
if app.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
|
38
|
+
app.config.assets.precompile += %w( actioncable.js actioncable.esm.js )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
initializer "action_cable.set_configs" do |app|
|
44
|
+
options = app.config.action_cable
|
45
|
+
options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
|
46
|
+
|
47
|
+
app.paths.add "config/cable", with: "config/cable.yml"
|
48
|
+
|
49
|
+
ActiveSupport.on_load(:action_cable) do
|
50
|
+
if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
|
51
|
+
self.cable = app.config_for(config_path).to_h.with_indifferent_access
|
52
|
+
end
|
53
|
+
|
54
|
+
previous_connection_class = connection_class
|
55
|
+
self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
|
56
|
+
self.filter_parameters += app.config.filter_parameters
|
57
|
+
|
58
|
+
options.each { |k, v| send("#{k}=", v) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
initializer "action_cable.routes" do
|
63
|
+
config.after_initialize do |app|
|
64
|
+
config = app.config
|
65
|
+
unless config.action_cable.mount_path.nil?
|
66
|
+
app.routes.prepend do
|
67
|
+
mount ActionCable.server => config.action_cable.mount_path, internal: true, anchor: true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
initializer "action_cable.set_work_hooks" do |app|
|
74
|
+
ActiveSupport.on_load(:action_cable) do
|
75
|
+
ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
|
76
|
+
app.executor.wrap(source: "application.action_cable") do
|
77
|
+
# If we took a while to get the lock, we may have been halted in the meantime.
|
78
|
+
# As we haven't started doing any real work yet, we should pretend that we never
|
79
|
+
# made it off the queue.
|
80
|
+
unless stopping?
|
81
|
+
inner.call
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
wrap = lambda do |_, inner|
|
87
|
+
app.executor.wrap(source: "application.action_cable", &inner)
|
88
|
+
end
|
89
|
+
ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
|
90
|
+
ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
|
91
|
+
|
92
|
+
app.reloader.before_class_unload do
|
93
|
+
ActionCable.server.restart
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
# Returns the currently loaded version of Action Cable as a `Gem::Version`.
|
7
|
+
def self.gem_version
|
8
|
+
Gem::Version.new VERSION::STRING
|
9
|
+
end
|
10
|
+
|
11
|
+
module VERSION
|
12
|
+
MAJOR = 8
|
13
|
+
MINOR = 0
|
14
|
+
TINY = 0
|
15
|
+
PRE = "alpha"
|
16
|
+
|
17
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Helpers
|
7
|
+
module ActionCableHelper
|
8
|
+
# Returns an "action-cable-url" meta tag with the value of the URL specified in
|
9
|
+
# your configuration. Ensure this is above your JavaScript tag:
|
10
|
+
#
|
11
|
+
# <head>
|
12
|
+
# <%= action_cable_meta_tag %>
|
13
|
+
# <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
|
14
|
+
# </head>
|
15
|
+
#
|
16
|
+
# This is then used by Action Cable to determine the URL of your WebSocket
|
17
|
+
# server. Your JavaScript can then connect to the server without needing to
|
18
|
+
# specify the URL directly:
|
19
|
+
#
|
20
|
+
# import Cable from "@rails/actioncable"
|
21
|
+
# window.Cable = Cable
|
22
|
+
# window.App = {}
|
23
|
+
# App.cable = Cable.createConsumer()
|
24
|
+
#
|
25
|
+
# Make sure to specify the correct server location in each of your environment
|
26
|
+
# config files:
|
27
|
+
#
|
28
|
+
# config.action_cable.mount_path = "/cable123"
|
29
|
+
# <%= action_cable_meta_tag %> would render:
|
30
|
+
# => <meta name="action-cable-url" content="/cable123" />
|
31
|
+
#
|
32
|
+
# config.action_cable.url = "ws://actioncable.com"
|
33
|
+
# <%= action_cable_meta_tag %> would render:
|
34
|
+
# => <meta name="action-cable-url" content="ws://actioncable.com" />
|
35
|
+
#
|
36
|
+
def action_cable_meta_tag
|
37
|
+
tag "meta", name: "action-cable-url", content: (
|
38
|
+
ActionCable.server.config.url ||
|
39
|
+
ActionCable.server.config.mount_path ||
|
40
|
+
raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/core_ext/module/redefine_method"
|
6
|
+
|
7
|
+
module ActionCable
|
8
|
+
# # Action Cable Remote Connections
|
9
|
+
#
|
10
|
+
# If you need to disconnect a given connection, you can go through the
|
11
|
+
# RemoteConnections. You can find the connections you're looking for by
|
12
|
+
# searching for the identifier declared on the connection. For example:
|
13
|
+
#
|
14
|
+
# module ApplicationCable
|
15
|
+
# class Connection < ActionCable::Connection::Base
|
16
|
+
# identified_by :current_user
|
17
|
+
# ....
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
|
22
|
+
#
|
23
|
+
# This will disconnect all the connections established for `User.find(1)`,
|
24
|
+
# across all servers running on all machines, because it uses the internal
|
25
|
+
# channel that all of these servers are subscribed to.
|
26
|
+
#
|
27
|
+
# By default, server sends a "disconnect" message with "reconnect" flag set to
|
28
|
+
# true. You can override it by specifying the `reconnect` option:
|
29
|
+
#
|
30
|
+
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false)
|
31
|
+
class RemoteConnections
|
32
|
+
attr_reader :server
|
33
|
+
|
34
|
+
def initialize(server)
|
35
|
+
@server = server
|
36
|
+
end
|
37
|
+
|
38
|
+
def where(identifier)
|
39
|
+
RemoteConnection.new(server, identifier)
|
40
|
+
end
|
41
|
+
|
42
|
+
# # Action Cable Remote Connection
|
43
|
+
#
|
44
|
+
# Represents a single remote connection found via
|
45
|
+
# `ActionCable.server.remote_connections.where(*)`. Exists solely for the
|
46
|
+
# purpose of calling #disconnect on that connection.
|
47
|
+
class RemoteConnection
|
48
|
+
class InvalidIdentifiersError < StandardError; end
|
49
|
+
|
50
|
+
include Connection::Identification, Connection::InternalChannel
|
51
|
+
|
52
|
+
def initialize(server, ids)
|
53
|
+
@server = server
|
54
|
+
set_identifier_instance_vars(ids)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Uses the internal channel to disconnect the connection.
|
58
|
+
def disconnect(reconnect: true)
|
59
|
+
server.broadcast internal_channel, { type: "disconnect", reconnect: reconnect }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns all the identifiers that were applied to this connection.
|
63
|
+
redefine_method :identifiers do
|
64
|
+
server.connection_identifiers
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
attr_reader :server
|
69
|
+
|
70
|
+
private
|
71
|
+
def set_identifier_instance_vars(ids)
|
72
|
+
raise InvalidIdentifiersError unless valid_identifiers?(ids)
|
73
|
+
ids.each { |k, v| instance_variable_set("@#{k}", v) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def valid_identifiers?(ids)
|
77
|
+
keys = ids.keys
|
78
|
+
identifiers.all? { |id| keys.include?(id) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|