omg-actioncable 8.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +511 -0
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +335 -0
  9. data/lib/action_cable/channel/broadcasting.rb +50 -0
  10. data/lib/action_cable/channel/callbacks.rb +76 -0
  11. data/lib/action_cable/channel/naming.rb +28 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +215 -0
  14. data/lib/action_cable/channel/test_case.rb +356 -0
  15. data/lib/action_cable/connection/authorization.rb +18 -0
  16. data/lib/action_cable/connection/base.rb +294 -0
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +159 -0
  19. data/lib/action_cable/connection/identification.rb +51 -0
  20. data/lib/action_cable/connection/internal_channel.rb +50 -0
  21. data/lib/action_cable/connection/message_buffer.rb +57 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +85 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
  26. data/lib/action_cable/connection/test_case.rb +246 -0
  27. data/lib/action_cable/connection/web_socket.rb +45 -0
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +98 -0
  30. data/lib/action_cable/gem_version.rb +19 -0
  31. data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
  32. data/lib/action_cable/remote_connections.rb +82 -0
  33. data/lib/action_cable/server/base.rb +109 -0
  34. data/lib/action_cable/server/broadcasting.rb +62 -0
  35. data/lib/action_cable/server/configuration.rb +70 -0
  36. data/lib/action_cable/server/connections.rb +44 -0
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
  38. data/lib/action_cable/server/worker.rb +75 -0
  39. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  40. data/lib/action_cable/subscription_adapter/base.rb +36 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +39 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
  44. data/lib/action_cable/subscription_adapter/redis.rb +256 -0
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +41 -0
  47. data/lib/action_cable/test_case.rb +13 -0
  48. data/lib/action_cable/test_helper.rb +163 -0
  49. data/lib/action_cable/version.rb +12 -0
  50. data/lib/action_cable.rb +80 -0
  51. data/lib/rails/generators/channel/USAGE +19 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +127 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +181 -0
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Connection
7
+ #--
8
+ # This class is heavily based on faye-websocket-ruby
9
+ #
10
+ # Copyright (c) 2010-2015 James Coglan
11
+ class Stream # :nodoc:
12
+ def initialize(event_loop, socket)
13
+ @event_loop = event_loop
14
+ @socket_object = socket
15
+ @stream_send = socket.env["stream.send"]
16
+
17
+ @rack_hijack_io = nil
18
+ @write_lock = Mutex.new
19
+
20
+ @write_head = nil
21
+ @write_buffer = Queue.new
22
+ end
23
+
24
+ def each(&callback)
25
+ @stream_send ||= callback
26
+ end
27
+
28
+ def close
29
+ shutdown
30
+ @socket_object.client_gone
31
+ end
32
+
33
+ def shutdown
34
+ clean_rack_hijack
35
+ end
36
+
37
+ def write(data)
38
+ if @stream_send
39
+ return @stream_send.call(data)
40
+ end
41
+
42
+ if @write_lock.try_lock
43
+ begin
44
+ if @write_head.nil? && @write_buffer.empty?
45
+ written = @rack_hijack_io.write_nonblock(data, exception: false)
46
+
47
+ case written
48
+ when :wait_writable
49
+ # proceed below
50
+ when data.bytesize
51
+ return data.bytesize
52
+ else
53
+ @write_head = data.byteslice(written, data.bytesize)
54
+ @event_loop.writes_pending @rack_hijack_io
55
+
56
+ return data.bytesize
57
+ end
58
+ end
59
+ ensure
60
+ @write_lock.unlock
61
+ end
62
+ end
63
+
64
+ @write_buffer << data
65
+ @event_loop.writes_pending @rack_hijack_io
66
+
67
+ data.bytesize
68
+ rescue EOFError, Errno::ECONNRESET
69
+ @socket_object.client_gone
70
+ end
71
+
72
+ def flush_write_buffer
73
+ @write_lock.synchronize do
74
+ loop do
75
+ if @write_head.nil?
76
+ return true if @write_buffer.empty?
77
+ @write_head = @write_buffer.pop
78
+ end
79
+
80
+ written = @rack_hijack_io.write_nonblock(@write_head, exception: false)
81
+ case written
82
+ when :wait_writable
83
+ return false
84
+ when @write_head.bytesize
85
+ @write_head = nil
86
+ else
87
+ @write_head = @write_head.byteslice(written, @write_head.bytesize)
88
+ return false
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def receive(data)
95
+ @socket_object.parse(data)
96
+ end
97
+
98
+ def hijack_rack_socket
99
+ return unless @socket_object.env["rack.hijack"]
100
+
101
+ # This should return the underlying io according to the SPEC:
102
+ @rack_hijack_io = @socket_object.env["rack.hijack"].call
103
+ # Retain existing behavior if required:
104
+ @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
105
+
106
+ @event_loop.attach(@rack_hijack_io, self)
107
+ end
108
+
109
+ private
110
+ def clean_rack_hijack
111
+ return unless @rack_hijack_io
112
+ @event_loop.detach(@rack_hijack_io, self)
113
+ @rack_hijack_io = nil
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "nio"
6
+
7
+ module ActionCable
8
+ module Connection
9
+ class StreamEventLoop
10
+ def initialize
11
+ @nio = @executor = @thread = nil
12
+ @map = {}
13
+ @stopping = false
14
+ @todo = Queue.new
15
+
16
+ @spawn_mutex = Mutex.new
17
+ end
18
+
19
+ def timer(interval, &block)
20
+ Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
21
+ end
22
+
23
+ def post(task = nil, &block)
24
+ task ||= block
25
+
26
+ spawn
27
+ @executor << task
28
+ end
29
+
30
+ def attach(io, stream)
31
+ @todo << lambda do
32
+ @map[io] = @nio.register(io, :r)
33
+ @map[io].value = stream
34
+ end
35
+ wakeup
36
+ end
37
+
38
+ def detach(io, stream)
39
+ @todo << lambda do
40
+ @nio.deregister io
41
+ @map.delete io
42
+ io.close
43
+ end
44
+ wakeup
45
+ end
46
+
47
+ def writes_pending(io)
48
+ @todo << lambda do
49
+ if monitor = @map[io]
50
+ monitor.interests = :rw
51
+ end
52
+ end
53
+ wakeup
54
+ end
55
+
56
+ def stop
57
+ @stopping = true
58
+ wakeup if @nio
59
+ end
60
+
61
+ private
62
+ def spawn
63
+ return if @thread && @thread.status
64
+
65
+ @spawn_mutex.synchronize do
66
+ return if @thread && @thread.status
67
+
68
+ @nio ||= NIO::Selector.new
69
+
70
+ @executor ||= Concurrent::ThreadPoolExecutor.new(
71
+ min_threads: 1,
72
+ max_threads: 10,
73
+ max_queue: 0,
74
+ )
75
+
76
+ @thread = Thread.new { run }
77
+
78
+ return true
79
+ end
80
+ end
81
+
82
+ def wakeup
83
+ spawn || @nio.wakeup
84
+ end
85
+
86
+ def run
87
+ loop do
88
+ if @stopping
89
+ @nio.close
90
+ break
91
+ end
92
+
93
+ until @todo.empty?
94
+ @todo.pop(true).call
95
+ end
96
+
97
+ next unless monitors = @nio.select
98
+
99
+ monitors.each do |monitor|
100
+ io = monitor.io
101
+ stream = monitor.value
102
+
103
+ begin
104
+ if monitor.writable?
105
+ if stream.flush_write_buffer
106
+ monitor.interests = :r
107
+ end
108
+ next unless monitor.readable?
109
+ end
110
+
111
+ incoming = io.read_nonblock(4096, exception: false)
112
+ case incoming
113
+ when :wait_readable
114
+ next
115
+ when nil
116
+ stream.close
117
+ else
118
+ stream.receive incoming
119
+ end
120
+ rescue
121
+ # We expect one of EOFError or Errno::ECONNRESET in normal operation (when the
122
+ # client goes away). But if anything else goes wrong, this is still the best way
123
+ # to handle it.
124
+ begin
125
+ stream.close
126
+ rescue
127
+ @nio.deregister io
128
+ @map.delete io
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+
7
+ module ActionCable
8
+ module Connection
9
+ # # Action Cable Connection Subscriptions
10
+ #
11
+ # Collection class for all the channel subscriptions established on a given
12
+ # connection. Responsible for routing incoming commands that arrive on the
13
+ # connection to the proper channel.
14
+ class Subscriptions # :nodoc:
15
+ def initialize(connection)
16
+ @connection = connection
17
+ @subscriptions = {}
18
+ end
19
+
20
+ def execute_command(data)
21
+ case data["command"]
22
+ when "subscribe" then add data
23
+ when "unsubscribe" then remove data
24
+ when "message" then perform_action data
25
+ else
26
+ logger.error "Received unrecognized command in #{data.inspect}"
27
+ end
28
+ rescue Exception => e
29
+ @connection.rescue_with_handler(e)
30
+ logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
31
+ end
32
+
33
+ def add(data)
34
+ id_key = data["identifier"]
35
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
36
+
37
+ return if subscriptions.key?(id_key)
38
+
39
+ subscription_klass = id_options[:channel].safe_constantize
40
+
41
+ if subscription_klass && ActionCable::Channel::Base > subscription_klass
42
+ subscription = subscription_klass.new(connection, id_key, id_options)
43
+ subscriptions[id_key] = subscription
44
+ subscription.subscribe_to_channel
45
+ else
46
+ logger.error "Subscription class not found: #{id_options[:channel].inspect}"
47
+ end
48
+ end
49
+
50
+ def remove(data)
51
+ logger.info "Unsubscribing from channel: #{data['identifier']}"
52
+ remove_subscription find(data)
53
+ end
54
+
55
+ def remove_subscription(subscription)
56
+ subscription.unsubscribe_from_channel
57
+ subscriptions.delete(subscription.identifier)
58
+ end
59
+
60
+ def perform_action(data)
61
+ find(data).perform_action ActiveSupport::JSON.decode(data["data"])
62
+ end
63
+
64
+ def identifiers
65
+ subscriptions.keys
66
+ end
67
+
68
+ def unsubscribe_from_all
69
+ subscriptions.each { |id, channel| remove_subscription(channel) }
70
+ end
71
+
72
+ private
73
+ attr_reader :connection, :subscriptions
74
+ delegate :logger, to: :connection
75
+
76
+ def find(data)
77
+ if subscription = subscriptions[data["identifier"]]
78
+ subscription
79
+ else
80
+ raise "Unable to find subscription with identifier: #{data['identifier']}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Connection
7
+ # # Action Cable Connection TaggedLoggerProxy
8
+ #
9
+ # Allows the use of per-connection tags against the server logger. This wouldn't
10
+ # work using the traditional ActiveSupport::TaggedLogging enhanced Rails.logger,
11
+ # as that logger will reset the tags between requests. The connection is
12
+ # long-lived, so it needs its own set of tags for its independent duration.
13
+ class TaggedLoggerProxy
14
+ attr_reader :tags
15
+
16
+ def initialize(logger, tags:)
17
+ @logger = logger
18
+ @tags = tags.flatten
19
+ end
20
+
21
+ def add_tags(*tags)
22
+ @tags += tags.flatten
23
+ @tags = @tags.uniq
24
+ end
25
+
26
+ def tag(logger, &block)
27
+ if logger.respond_to?(:tagged)
28
+ current_tags = tags - logger.formatter.current_tags
29
+ logger.tagged(*current_tags, &block)
30
+ else
31
+ yield
32
+ end
33
+ end
34
+
35
+ %i( debug info warn error fatal unknown ).each do |severity|
36
+ define_method(severity) do |message = nil, &block|
37
+ log severity, message, &block
38
+ end
39
+ end
40
+
41
+ private
42
+ def log(type, message, &block) # :doc:
43
+ tag(@logger) { @logger.send type, message, &block }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,246 @@
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 TestRequest < ActionDispatch::TestRequest
54
+ attr_accessor :session, :cookie_jar
55
+ end
56
+
57
+ module TestConnection
58
+ attr_reader :logger, :request
59
+
60
+ def initialize(request)
61
+ inner_logger = ActiveSupport::Logger.new(StringIO.new)
62
+ tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
63
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
64
+ @request = request
65
+ @env = request.env
66
+ end
67
+ end
68
+
69
+ # # Action Cable Connection TestCase
70
+ #
71
+ # Unit test Action Cable connections.
72
+ #
73
+ # Useful to check whether a connection's `identified_by` gets assigned properly
74
+ # and that any improper connection requests are rejected.
75
+ #
76
+ # ## Basic example
77
+ #
78
+ # Unit tests are written as follows:
79
+ #
80
+ # 1. Simulate a connection attempt by calling `connect`.
81
+ # 2. Assert state, e.g. identifiers, has been assigned.
82
+ #
83
+ #
84
+ # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
85
+ # def test_connects_with_proper_cookie
86
+ # # Simulate the connection request with a cookie.
87
+ # cookies["user_id"] = users(:john).id
88
+ #
89
+ # connect
90
+ #
91
+ # # Assert the connection identifier matches the fixture.
92
+ # assert_equal users(:john).id, connection.user.id
93
+ # end
94
+ #
95
+ # def test_rejects_connection_without_proper_cookie
96
+ # assert_reject_connection { connect }
97
+ # end
98
+ # end
99
+ #
100
+ # `connect` accepts additional information about the HTTP request with the
101
+ # `params`, `headers`, `session`, and Rack `env` options.
102
+ #
103
+ # def test_connect_with_headers_and_query_string
104
+ # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
105
+ #
106
+ # assert_equal "1", connection.user.id
107
+ # assert_equal "secret-my", connection.token
108
+ # end
109
+ #
110
+ # def test_connect_with_params
111
+ # connect params: { user_id: 1 }
112
+ #
113
+ # assert_equal "1", connection.user.id
114
+ # end
115
+ #
116
+ # You can also set up the correct cookies before the connection request:
117
+ #
118
+ # def test_connect_with_cookies
119
+ # # Plain cookies:
120
+ # cookies["user_id"] = 1
121
+ #
122
+ # # Or signed/encrypted:
123
+ # # cookies.signed["user_id"] = 1
124
+ # # cookies.encrypted["user_id"] = 1
125
+ #
126
+ # connect
127
+ #
128
+ # assert_equal "1", connection.user_id
129
+ # end
130
+ #
131
+ # ## Connection is automatically inferred
132
+ #
133
+ # ActionCable::Connection::TestCase will automatically infer the connection
134
+ # under test from the test class name. If the channel cannot be inferred from
135
+ # the test class name, you can explicitly set it with `tests`.
136
+ #
137
+ # class ConnectionTest < ActionCable::Connection::TestCase
138
+ # tests ApplicationCable::Connection
139
+ # end
140
+ #
141
+ class TestCase < ActiveSupport::TestCase
142
+ module Behavior
143
+ extend ActiveSupport::Concern
144
+
145
+ DEFAULT_PATH = "/cable"
146
+
147
+ include ActiveSupport::Testing::ConstantLookup
148
+ include Assertions
149
+
150
+ included do
151
+ class_attribute :_connection_class
152
+
153
+ attr_reader :connection
154
+
155
+ ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
156
+ end
157
+
158
+ module ClassMethods
159
+ def tests(connection)
160
+ case connection
161
+ when String, Symbol
162
+ self._connection_class = connection.to_s.camelize.constantize
163
+ when Module
164
+ self._connection_class = connection
165
+ else
166
+ raise NonInferrableConnectionError.new(connection)
167
+ end
168
+ end
169
+
170
+ def connection_class
171
+ if connection = self._connection_class
172
+ connection
173
+ else
174
+ tests determine_default_connection(name)
175
+ end
176
+ end
177
+
178
+ def determine_default_connection(name)
179
+ connection = determine_constant_from_test_name(name) do |constant|
180
+ Class === constant && constant < ActionCable::Connection::Base
181
+ end
182
+ raise NonInferrableConnectionError.new(name) if connection.nil?
183
+ connection
184
+ end
185
+ end
186
+
187
+ # Performs connection attempt to exert #connect on the connection under test.
188
+ #
189
+ # Accepts request path as the first argument and the following request options:
190
+ #
191
+ # * params – URL parameters (Hash)
192
+ # * headers – request headers (Hash)
193
+ # * session – session data (Hash)
194
+ # * env – additional Rack env configuration (Hash)
195
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
196
+ path ||= DEFAULT_PATH
197
+
198
+ connection = self.class.connection_class.allocate
199
+ connection.singleton_class.include(TestConnection)
200
+ connection.send(:initialize, build_test_request(path, **request_params))
201
+ connection.connect if connection.respond_to?(:connect)
202
+
203
+ # Only set instance variable if connected successfully
204
+ @connection = connection
205
+ end
206
+
207
+ # Exert #disconnect on the connection under test.
208
+ def disconnect
209
+ raise "Must be connected!" if connection.nil?
210
+
211
+ connection.disconnect if connection.respond_to?(:disconnect)
212
+ @connection = nil
213
+ end
214
+
215
+ def cookies
216
+ @cookie_jar ||= TestCookieJar.new
217
+ end
218
+
219
+ private
220
+ def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
221
+ wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
222
+
223
+ uri = URI.parse(path)
224
+
225
+ query_string = params.nil? ? uri.query : params.to_query
226
+
227
+ request_env = {
228
+ "QUERY_STRING" => query_string,
229
+ "PATH_INFO" => uri.path
230
+ }.merge(env)
231
+
232
+ if wrapped_headers.present?
233
+ ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
234
+ end
235
+
236
+ TestRequest.create(request_env).tap do |request|
237
+ request.session = session.with_indifferent_access
238
+ request.cookie_jar = cookies
239
+ end
240
+ end
241
+ end
242
+
243
+ include Behavior
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "websocket/driver"
6
+
7
+ module ActionCable
8
+ module Connection
9
+ # # Action Cable Connection WebSocket
10
+ #
11
+ # Wrap the real socket to minimize the externally-presented API
12
+ class WebSocket # :nodoc:
13
+ def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
14
+ @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil
15
+ end
16
+
17
+ def possible?
18
+ websocket
19
+ end
20
+
21
+ def alive?
22
+ websocket&.alive?
23
+ end
24
+
25
+ def transmit(...)
26
+ websocket&.transmit(...)
27
+ end
28
+
29
+ def close(...)
30
+ websocket&.close(...)
31
+ end
32
+
33
+ def protocol
34
+ websocket&.protocol
35
+ end
36
+
37
+ def rack_response
38
+ websocket&.rack_response
39
+ end
40
+
41
+ private
42
+ attr_reader :websocket
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ def self.deprecator # :nodoc:
7
+ @deprecator ||= ActiveSupport::Deprecation.new
8
+ end
9
+ end