omg-actioncable 8.0.0.alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +511 -0
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +335 -0
  9. data/lib/action_cable/channel/broadcasting.rb +50 -0
  10. data/lib/action_cable/channel/callbacks.rb +76 -0
  11. data/lib/action_cable/channel/naming.rb +28 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +215 -0
  14. data/lib/action_cable/channel/test_case.rb +356 -0
  15. data/lib/action_cable/connection/authorization.rb +18 -0
  16. data/lib/action_cable/connection/base.rb +294 -0
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +159 -0
  19. data/lib/action_cable/connection/identification.rb +51 -0
  20. data/lib/action_cable/connection/internal_channel.rb +50 -0
  21. data/lib/action_cable/connection/message_buffer.rb +57 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +85 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
  26. data/lib/action_cable/connection/test_case.rb +246 -0
  27. data/lib/action_cable/connection/web_socket.rb +45 -0
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +98 -0
  30. data/lib/action_cable/gem_version.rb +19 -0
  31. data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
  32. data/lib/action_cable/remote_connections.rb +82 -0
  33. data/lib/action_cable/server/base.rb +109 -0
  34. data/lib/action_cable/server/broadcasting.rb +62 -0
  35. data/lib/action_cable/server/configuration.rb +70 -0
  36. data/lib/action_cable/server/connections.rb +44 -0
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
  38. data/lib/action_cable/server/worker.rb +75 -0
  39. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  40. data/lib/action_cable/subscription_adapter/base.rb +36 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +39 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
  44. data/lib/action_cable/subscription_adapter/redis.rb +256 -0
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +41 -0
  47. data/lib/action_cable/test_case.rb +13 -0
  48. data/lib/action_cable/test_helper.rb +163 -0
  49. data/lib/action_cable/version.rb +12 -0
  50. data/lib/action_cable.rb +80 -0
  51. data/lib/rails/generators/channel/USAGE +19 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +127 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +181 -0
@@ -0,0 +1,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